@theia/ai-ide 1.74.0-next.9 → 1.74.0

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 (96) hide show
  1. package/lib/browser/ai-allow-all-mode-chat-banner.d.ts +68 -0
  2. package/lib/browser/ai-allow-all-mode-chat-banner.d.ts.map +1 -0
  3. package/lib/browser/ai-allow-all-mode-chat-banner.js +184 -0
  4. package/lib/browser/ai-allow-all-mode-chat-banner.js.map +1 -0
  5. package/lib/browser/ai-allow-all-mode-chat-banner.spec.d.ts +2 -0
  6. package/lib/browser/ai-allow-all-mode-chat-banner.spec.d.ts.map +1 -0
  7. package/lib/browser/ai-allow-all-mode-chat-banner.spec.js +170 -0
  8. package/lib/browser/ai-allow-all-mode-chat-banner.spec.js.map +1 -0
  9. package/lib/browser/ai-configuration/tools-configuration-widget.d.ts +3 -4
  10. package/lib/browser/ai-configuration/tools-configuration-widget.d.ts.map +1 -1
  11. package/lib/browser/ai-configuration/tools-configuration-widget.js +17 -10
  12. package/lib/browser/ai-configuration/tools-configuration-widget.js.map +1 -1
  13. package/lib/browser/ai-first-perspective-contribution.d.ts +6 -0
  14. package/lib/browser/ai-first-perspective-contribution.d.ts.map +1 -0
  15. package/lib/browser/ai-first-perspective-contribution.js +50 -0
  16. package/lib/browser/ai-first-perspective-contribution.js.map +1 -0
  17. package/lib/browser/ai-sessions-view-contribution.d.ts +13 -0
  18. package/lib/browser/ai-sessions-view-contribution.d.ts.map +1 -0
  19. package/lib/browser/ai-sessions-view-contribution.js +69 -0
  20. package/lib/browser/ai-sessions-view-contribution.js.map +1 -0
  21. package/lib/browser/ai-sessions-widget.d.ts +27 -0
  22. package/lib/browser/ai-sessions-widget.d.ts.map +1 -0
  23. package/lib/browser/ai-sessions-widget.js +131 -0
  24. package/lib/browser/ai-sessions-widget.js.map +1 -0
  25. package/lib/browser/chat-session-item-action-contribution.js +2 -2
  26. package/lib/browser/chat-session-item-action-contribution.js.map +1 -1
  27. package/lib/browser/chat-session-item.d.ts +15 -0
  28. package/lib/browser/chat-session-item.d.ts.map +1 -1
  29. package/lib/browser/chat-session-item.js +39 -18
  30. package/lib/browser/chat-session-item.js.map +1 -1
  31. package/lib/browser/chat-session-list-components.d.ts +38 -0
  32. package/lib/browser/chat-session-list-components.d.ts.map +1 -0
  33. package/lib/browser/chat-session-list-components.js +87 -0
  34. package/lib/browser/chat-session-list-components.js.map +1 -0
  35. package/lib/browser/chat-session-list-service.d.ts +34 -0
  36. package/lib/browser/chat-session-list-service.d.ts.map +1 -0
  37. package/lib/browser/chat-session-list-service.js +251 -0
  38. package/lib/browser/chat-session-list-service.js.map +1 -0
  39. package/lib/browser/chat-session-list-service.spec.d.ts +2 -0
  40. package/lib/browser/chat-session-list-service.spec.d.ts.map +1 -0
  41. package/lib/browser/{chat-sessions-welcome-message-provider.spec.js → chat-session-list-service.spec.js} +133 -39
  42. package/lib/browser/chat-session-list-service.spec.js.map +1 -0
  43. package/lib/browser/chat-session-tooltip.d.ts +2 -2
  44. package/lib/browser/chat-session-tooltip.d.ts.map +1 -1
  45. package/lib/browser/chat-session-tooltip.js +33 -10
  46. package/lib/browser/chat-session-tooltip.js.map +1 -1
  47. package/lib/browser/chat-sessions-welcome-message-provider.d.ts +9 -35
  48. package/lib/browser/chat-sessions-welcome-message-provider.d.ts.map +1 -1
  49. package/lib/browser/chat-sessions-welcome-message-provider.js +51 -205
  50. package/lib/browser/chat-sessions-welcome-message-provider.js.map +1 -1
  51. package/lib/browser/context-file-validation-service-impl.spec.js +2 -2
  52. package/lib/browser/context-file-validation-service-impl.spec.js.map +1 -1
  53. package/lib/browser/frontend-module.d.ts.map +1 -1
  54. package/lib/browser/frontend-module.js +20 -0
  55. package/lib/browser/frontend-module.js.map +1 -1
  56. package/lib/browser/workspace-functions.d.ts +5 -6
  57. package/lib/browser/workspace-functions.d.ts.map +1 -1
  58. package/lib/browser/workspace-functions.js +24 -9
  59. package/lib/browser/workspace-functions.js.map +1 -1
  60. package/lib/browser/workspace-functions.spec.js +29 -29
  61. package/lib/browser/workspace-functions.spec.js.map +1 -1
  62. package/lib/browser/workspace-launch-provider.spec.js +3 -3
  63. package/lib/browser/workspace-launch-provider.spec.js.map +1 -1
  64. package/lib/browser/workspace-search-provider.d.ts.map +1 -1
  65. package/lib/browser/workspace-search-provider.js +28 -20
  66. package/lib/browser/workspace-search-provider.js.map +1 -1
  67. package/lib/browser/workspace-search-provider.spec.js +68 -0
  68. package/lib/browser/workspace-search-provider.spec.js.map +1 -1
  69. package/lib/browser/workspace-task-provider.spec.js +4 -4
  70. package/lib/browser/workspace-task-provider.spec.js.map +1 -1
  71. package/package.json +25 -25
  72. package/src/browser/ai-allow-all-mode-chat-banner.spec.ts +205 -0
  73. package/src/browser/ai-allow-all-mode-chat-banner.tsx +217 -0
  74. package/src/browser/ai-configuration/tools-configuration-widget.tsx +20 -13
  75. package/src/browser/ai-first-perspective-contribution.ts +48 -0
  76. package/src/browser/ai-sessions-view-contribution.ts +67 -0
  77. package/src/browser/ai-sessions-widget.tsx +151 -0
  78. package/src/browser/chat-session-item-action-contribution.ts +2 -2
  79. package/src/browser/chat-session-item.tsx +82 -27
  80. package/src/browser/chat-session-list-components.tsx +136 -0
  81. package/src/browser/{chat-sessions-welcome-message-provider.spec.ts → chat-session-list-service.spec.ts} +136 -30
  82. package/src/browser/chat-session-list-service.ts +261 -0
  83. package/src/browser/chat-session-tooltip.ts +31 -11
  84. package/src/browser/chat-sessions-welcome-message-provider.tsx +78 -268
  85. package/src/browser/context-file-validation-service-impl.spec.ts +3 -3
  86. package/src/browser/frontend-module.ts +23 -0
  87. package/src/browser/style/index.css +150 -3
  88. package/src/browser/workspace-functions.spec.ts +36 -37
  89. package/src/browser/workspace-functions.ts +8 -9
  90. package/src/browser/workspace-launch-provider.spec.ts +5 -5
  91. package/src/browser/workspace-search-provider.spec.ts +101 -0
  92. package/src/browser/workspace-search-provider.ts +34 -23
  93. package/src/browser/workspace-task-provider.spec.ts +6 -7
  94. package/lib/browser/chat-sessions-welcome-message-provider.spec.d.ts +0 -2
  95. package/lib/browser/chat-sessions-welcome-message-provider.spec.d.ts.map +0 -1
  96. package/lib/browser/chat-sessions-welcome-message-provider.spec.js.map +0 -1
@@ -0,0 +1,217 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource and others.
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 * as React from '@theia/core/shared/react';
18
+ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
19
+ import { codicon } from '@theia/core/lib/browser';
20
+ import { DisposableCollection, Emitter, Event } from '@theia/core';
21
+ import { nls } from '@theia/core/lib/common/nls';
22
+ import { PreferenceService } from '@theia/core/lib/common';
23
+ import { ChatBannerProvider } from '@theia/ai-chat-ui/lib/browser/chat-banner-provider';
24
+ import {
25
+ DEFAULT_TOOL_CONFIRMATION_PREFERENCE,
26
+ TOOL_CONFIRMATION_PREFERENCE,
27
+ ToolConfirmationMode
28
+ } from '@theia/ai-chat/lib/common/chat-tool-preferences';
29
+
30
+ interface AiSessionOverride {
31
+ /** Human-readable label, e.g. "Default tool confirmation: always_allow". */
32
+ label: string;
33
+ /** Whether this override bypasses AI tool confirmations globally. */
34
+ bypass: boolean;
35
+ }
36
+
37
+ /**
38
+ * Persistent strip rendered above the chat content whenever an AI tool confirmation
39
+ * preference (`ai-features.chat.defaultToolConfirmation` or `ai-features.chat.toolConfirmation`)
40
+ * is active in the session scope (typically set via `--session-preference`). When the
41
+ * global default is forced to "Allow All" the strip is shown in error styling as the
42
+ * Theia AI Allow-All Mode banner; per-tool confirmation overrides use warning styling.
43
+ *
44
+ * Other AI-namespaced session overrides (e.g. forced-on AI features, default chat agent)
45
+ * are reported by the generic Session Preferences status bar item and intentionally not
46
+ * duplicated here.
47
+ */
48
+ @injectable()
49
+ export class AiAllowAllModeChatBanner implements ChatBannerProvider {
50
+
51
+ readonly priority = 1000;
52
+
53
+ @inject(PreferenceService)
54
+ protected readonly preferenceService: PreferenceService;
55
+
56
+ protected readonly toDispose = new DisposableCollection();
57
+ protected readonly onDidChangeEmitter = new Emitter<void>();
58
+ readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
59
+
60
+ /**
61
+ * Only tool confirmation preferences are watched here. Other AI session overrides
62
+ * (enable AI, agent mode, default chat agent, etc.) are surfaced in the generic
63
+ * Session Preferences status bar item to keep the banner from duplicating the list.
64
+ */
65
+ protected readonly watchedKeys: ReadonlyArray<string> = [
66
+ DEFAULT_TOOL_CONFIRMATION_PREFERENCE,
67
+ TOOL_CONFIRMATION_PREFERENCE
68
+ ];
69
+
70
+ /**
71
+ * Process-lifetime dismissal flag. Reset on the next launch so the user is
72
+ * always reminded once per process. Also automatically cleared when the override
73
+ * set changes in any way after a dismissal (see {@link init}), so a previously
74
+ * dismissed strip cannot silently hide new or escalated overrides.
75
+ */
76
+ protected dismissed = false;
77
+
78
+ /**
79
+ * Signature of the override set at the moment the user dismissed the strip.
80
+ * `undefined` while the strip is not dismissed.
81
+ */
82
+ protected dismissedSignature: string | undefined;
83
+
84
+ @postConstruct()
85
+ protected init(): void {
86
+ const watched = new Set(this.watchedKeys);
87
+ this.toDispose.push(this.preferenceService.onPreferenceChanged(e => {
88
+ if (!watched.has(e.preferenceName)) {
89
+ return;
90
+ }
91
+ if (this.dismissed && this.overrideSignature() !== this.dismissedSignature) {
92
+ // The override set has changed since the user dismissed the strip
93
+ // (new override added, existing value changed, escalation into a bypass, ...).
94
+ // Re-surface the strip so the change cannot hide behind the earlier dismissal.
95
+ this.dismissed = false;
96
+ this.dismissedSignature = undefined;
97
+ }
98
+ this.onDidChangeEmitter.fire();
99
+ }));
100
+ }
101
+
102
+ /**
103
+ * Stable snapshot of the watched preferences' current session values. Used to
104
+ * detect changes to the override set for the dismissal-reset logic; not for
105
+ * anything user-visible so the format is intentionally simple.
106
+ */
107
+ protected overrideSignature(): string {
108
+ return this.watchedKeys
109
+ .map(key => `${key}=${JSON.stringify(this.preferenceService.inspect(key)?.sessionValue ?? undefined)}`)
110
+ .join('|');
111
+ }
112
+
113
+ renderBanner(): React.ReactNode | undefined {
114
+ if (this.dismissed) {
115
+ return undefined;
116
+ }
117
+ const overrides = this.collectOverrides();
118
+ if (overrides.length === 0) {
119
+ return undefined;
120
+ }
121
+ const isAllowAll = overrides.some(override => override.bypass);
122
+ const className = `theia-ai-allow-all-mode-strip ${isAllowAll ? 'is-allow-all' : 'is-overrides'}`;
123
+ const title = isAllowAll
124
+ ? nls.localize('theia/ai/ide/allowAllMode/title', 'Theia AI Allow-All Mode')
125
+ : nls.localize('theia/ai/ide/toolConfirmationOverrides/title', 'AI Tool Confirmation Overrides');
126
+ const tooltipLines = [
127
+ isAllowAll
128
+ ? nls.localize('theia/ai/ide/allowAllMode/stripTooltip',
129
+ 'AI tool calls run without confirmation in this session. Only enable in environments you trust.')
130
+ : nls.localize('theia/ai/ide/toolConfirmationOverrides/stripTooltip',
131
+ 'One or more AI tools are auto-approved for this session via command-line flags.'),
132
+ '',
133
+ ...overrides.map(override => `• ${override.label}`),
134
+ '',
135
+ nls.localize('theia/ai/ide/allowAllMode/stripTooltipFooter',
136
+ 'Set via --session-preference. Restart without the flag to restore your saved settings.')
137
+ ];
138
+ return <div
139
+ className={className}
140
+ title={tooltipLines.join('\n')}
141
+ role='status'
142
+ aria-live='polite'>
143
+ <span className={`theia-ai-allow-all-mode-strip-icon ${codicon('warning')}`}></span>
144
+ <span className='theia-ai-allow-all-mode-strip-title'>{title}</span>
145
+ <button
146
+ type='button'
147
+ className={`theia-ai-allow-all-mode-strip-dismiss ${codicon('close')}`}
148
+ title={nls.localizeByDefault('Hide for this session')}
149
+ aria-label={nls.localizeByDefault('Hide for this session')}
150
+ onClick={this.handleDismiss}
151
+ />
152
+ </div>;
153
+ }
154
+
155
+ /** AI-relevant preferences currently coming from the session scope. */
156
+ protected collectOverrides(): AiSessionOverride[] {
157
+ const overrides: AiSessionOverride[] = [];
158
+ for (const key of this.watchedKeys) {
159
+ const sessionValue = this.preferenceService.inspect(key)?.sessionValue;
160
+ if (sessionValue === undefined) {
161
+ continue;
162
+ }
163
+ overrides.push({
164
+ label: this.describe(key, sessionValue),
165
+ bypass: this.isBypassValue(key, sessionValue)
166
+ });
167
+ }
168
+ return overrides;
169
+ }
170
+
171
+ /**
172
+ * Returns `true` only when the global default is forced to Allow-All. Per-tool
173
+ * `always_allow` entries via `TOOL_CONFIRMATION_PREFERENCE` are scoped exceptions
174
+ * and remain in the banner as informational (warning) entries rather than escalating
175
+ * the whole strip to the red "Allow-All Mode" treatment.
176
+ */
177
+ protected isBypassValue(key: string, value: unknown): boolean {
178
+ if (key === DEFAULT_TOOL_CONFIRMATION_PREFERENCE) {
179
+ return value === ToolConfirmationMode.ALWAYS_ALLOW;
180
+ }
181
+ return false;
182
+ }
183
+
184
+ protected describe(key: string, value: unknown): string {
185
+ switch (key) {
186
+ case DEFAULT_TOOL_CONFIRMATION_PREFERENCE:
187
+ return nls.localize('theia/ai/ide/sessionOverride/defaultToolConfirmation',
188
+ 'Default tool confirmation: {0}', String(value));
189
+ case TOOL_CONFIRMATION_PREFERENCE: {
190
+ if (value && typeof value === 'object') {
191
+ const entries = Object.entries(value as Record<string, unknown>)
192
+ .map(([tool, mode]) => `${tool}=${mode}`)
193
+ .join(', ');
194
+ return nls.localize('theia/ai/ide/sessionOverride/toolConfirmation',
195
+ 'Per-tool confirmation overrides: {0}', entries);
196
+ }
197
+ return nls.localize('theia/ai/ide/sessionOverride/toolConfirmationGeneric',
198
+ 'Per-tool confirmation overrides set');
199
+ }
200
+ default:
201
+ return `${key} = ${JSON.stringify(value)}`;
202
+ }
203
+ }
204
+
205
+ protected handleDismiss = (e: React.MouseEvent<HTMLButtonElement>) => {
206
+ e.stopPropagation();
207
+ this.dismissed = true;
208
+ // Snapshot the signature so a subsequent change to the override set is detected
209
+ // in the preference-change listener and un-dismisses the strip automatically.
210
+ this.dismissedSignature = this.overrideSignature();
211
+ this.onDidChangeEmitter.fire();
212
+ };
213
+
214
+ dispose(): void {
215
+ this.toDispose.dispose();
216
+ }
217
+ }
@@ -17,8 +17,8 @@
17
17
  import { ConfirmDialog } from '@theia/core/lib/browser';
18
18
  import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
19
19
  import * as React from '@theia/core/shared/react';
20
- import { ToolInvocationRegistry, ToolRequest } from '@theia/ai-core';
21
- import { nls, PreferenceService } from '@theia/core';
20
+ import { AiConfigurationService, ToolInvocationRegistry, ToolRequest } from '@theia/ai-core';
21
+ import { nls } from '@theia/core';
22
22
  import { ToolConfirmationManager } from '@theia/ai-chat/lib/browser/chat-tool-preference-bindings';
23
23
  import { ShellCommandPermissionService } from '@theia/ai-terminal/lib/browser/shell-command-permission-service';
24
24
  import {
@@ -47,8 +47,8 @@ export class AIToolsConfigurationWidget extends AITableConfigurationWidget<ToolI
47
47
  @inject(ToolConfirmationManager)
48
48
  protected readonly confirmationManager: ToolConfirmationManager;
49
49
 
50
- @inject(PreferenceService)
51
- protected readonly preferenceService: PreferenceService;
50
+ @inject(AiConfigurationService)
51
+ protected readonly aiConfigurationService: AiConfigurationService;
52
52
 
53
53
  @inject(ToolInvocationRegistry)
54
54
  protected readonly toolInvocationRegistry: ToolInvocationRegistry;
@@ -74,18 +74,18 @@ export class AIToolsConfigurationWidget extends AITableConfigurationWidget<ToolI
74
74
 
75
75
  this.loadData().then(() => this.update());
76
76
  this.toDispose.pushAll([
77
- this.preferenceService.onPreferenceChanged(async e => {
78
- if (e.preferenceName === TOOL_CONFIRMATION_PREFERENCE
79
- || e.preferenceName === DEFAULT_TOOL_CONFIRMATION_PREFERENCE) {
77
+ this.aiConfigurationService.onDidChange(async e => {
78
+ if (e.affectsPreference(TOOL_CONFIRMATION_PREFERENCE)
79
+ || e.affectsPreference(DEFAULT_TOOL_CONFIRMATION_PREFERENCE)) {
80
80
  this.defaultState = await this.loadDefaultConfirmation();
81
81
  this.toolConfirmationModes = await this.loadToolConfigurationModes();
82
82
  this.update();
83
83
  }
84
- if (e.preferenceName === SHELL_COMMAND_ALLOWLIST_PREFERENCE) {
84
+ if (e.affectsPreference(SHELL_COMMAND_ALLOWLIST_PREFERENCE)) {
85
85
  this.allowlistPatterns = this.shellCommandPermissionService.getAllowlistPatterns();
86
86
  this.update();
87
87
  }
88
- if (e.preferenceName === SHELL_COMMAND_DENYLIST_PREFERENCE) {
88
+ if (e.affectsPreference(SHELL_COMMAND_DENYLIST_PREFERENCE)) {
89
89
  this.denylistPatterns = this.shellCommandPermissionService.getDenylistPatterns();
90
90
  this.update();
91
91
  }
@@ -98,6 +98,7 @@ export class AIToolsConfigurationWidget extends AITableConfigurationWidget<ToolI
98
98
  }
99
99
 
100
100
  protected async loadData(): Promise<void> {
101
+ await this.aiConfigurationService.ready;
101
102
  await this.loadItems();
102
103
  this.defaultState = await this.loadDefaultConfirmation();
103
104
  this.toolConfirmationModes = await this.loadToolConfigurationModes();
@@ -374,7 +375,8 @@ export class AIToolsConfigurationWidget extends AITableConfigurationWidget<ToolI
374
375
  }
375
376
 
376
377
  protected handleRemoveAllowlistPattern(pattern: string): void {
377
- this.shellCommandPermissionService.removeAllowlistPattern(pattern);
378
+ this.shellCommandPermissionService.removeAllowlistPattern(pattern)
379
+ .catch(error => console.error('Failed to remove allowlist pattern:', error));
378
380
  }
379
381
 
380
382
  protected handleAddDenylistPattern(): void {
@@ -388,12 +390,13 @@ export class AIToolsConfigurationWidget extends AITableConfigurationWidget<ToolI
388
390
  }
389
391
 
390
392
  protected handleRemoveDenylistPattern(pattern: string): void {
391
- this.shellCommandPermissionService.removeDenylistPattern(pattern);
393
+ this.shellCommandPermissionService.removeDenylistPattern(pattern)
394
+ .catch(error => console.error('Failed to remove denylist pattern:', error));
392
395
  }
393
396
 
394
397
  protected handleAddPatternToList(
395
398
  inputRef: React.RefObject<HTMLInputElement | null>,
396
- addFn: (pattern: string) => void,
399
+ addFn: (pattern: string) => Promise<void>,
397
400
  getFn: () => string[],
398
401
  setPatterns: (patterns: string[]) => void,
399
402
  setError: (error: string | undefined) => void
@@ -409,7 +412,11 @@ export class AIToolsConfigurationWidget extends AITableConfigurationWidget<ToolI
409
412
  }
410
413
 
411
414
  try {
412
- addFn(trimmed);
415
+ // Validation throws synchronously; a failed write rejects the returned promise.
416
+ addFn(trimmed).catch(error => {
417
+ setError(error instanceof Error ? error.message : 'Failed to save pattern');
418
+ this.update();
419
+ });
413
420
  input.value = '';
414
421
  setError(undefined);
415
422
  setPatterns(getFn());
@@ -0,0 +1,48 @@
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 { injectable } from '@theia/core/shared/inversify';
18
+ import { nls } from '@theia/core';
19
+ import { PerspectiveContribution, PerspectiveChromeOptions, PerspectiveService } from '@theia/core/lib/browser/perspective-service';
20
+ import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
21
+ import { EXPLORER_VIEW_CONTAINER_ID } from '@theia/navigator/lib/browser';
22
+ import { SCM_VIEW_CONTAINER_ID } from '@theia/scm/lib/browser/scm-contribution';
23
+ import { ChatViewWidget } from '@theia/ai-chat-ui/lib/browser/chat-view-widget';
24
+ import { AISessionsWidget } from './ai-sessions-widget';
25
+
26
+ export const AI_FIRST_PERSPECTIVE_ID = 'ai-first';
27
+
28
+ @injectable()
29
+ export class AIFirstPerspectiveContribution implements PerspectiveContribution {
30
+
31
+ registerPerspectives(service: PerspectiveService): void {
32
+ const chromeOptions: PerspectiveChromeOptions = {
33
+ collapseAreas: ['bottom']
34
+ };
35
+ service.registerPerspective({
36
+ id: AI_FIRST_PERSPECTIVE_ID,
37
+ label: nls.localize('theia/ai-ide/perspective/aiFirst', 'AI First'),
38
+ viewPlacements: new Map<string, ApplicationShell.Area>([
39
+ [ChatViewWidget.ID, 'main'],
40
+ [EXPLORER_VIEW_CONTAINER_ID, 'right'],
41
+ [SCM_VIEW_CONTAINER_ID, 'right'],
42
+ [AISessionsWidget.ID, 'left']
43
+ ]),
44
+ primaryViews: { right: EXPLORER_VIEW_CONTAINER_ID },
45
+ chromeOptions
46
+ });
47
+ }
48
+ }
@@ -0,0 +1,67 @@
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 { AI_CHAT_HOME, ChatCommands } from '@theia/ai-chat-ui/lib/browser/chat-view-commands';
18
+ import { AIViewContribution, ENABLE_AI_CONTEXT_KEY } from '@theia/ai-core/lib/browser';
19
+ import { Command, CommandRegistry, nls } from '@theia/core';
20
+ import { codicon } from '@theia/core/lib/browser';
21
+ import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
22
+ import { inject, injectable } from '@theia/core/shared/inversify';
23
+ import { AISessionsWidget } from './ai-sessions-widget';
24
+
25
+ export const AI_SESSIONS_TOGGLE_COMMAND_ID = 'aiSessions:toggle';
26
+
27
+ export const AI_SESSIONS_NEW_SESSION = Command.toLocalizedCommand({
28
+ id: 'aiSessions:newSession',
29
+ iconClass: codicon('add'),
30
+ category: ChatCommands.CHAT_CATEGORY,
31
+ label: 'New Chat'
32
+ }, 'theia/ai-ide/newChat', ChatCommands.CHAT_CATEGORY_KEY);
33
+
34
+ @injectable()
35
+ export class AISessionsViewContribution extends AIViewContribution<AISessionsWidget> implements TabBarToolbarContribution {
36
+
37
+ @inject(CommandRegistry)
38
+ protected readonly commandRegistry: CommandRegistry;
39
+
40
+ constructor() {
41
+ super({
42
+ widgetId: AISessionsWidget.ID,
43
+ widgetName: AISessionsWidget.LABEL,
44
+ defaultWidgetOptions: {
45
+ area: 'left'
46
+ },
47
+ toggleCommandId: AI_SESSIONS_TOGGLE_COMMAND_ID
48
+ });
49
+ }
50
+
51
+ override registerCommands(commands: CommandRegistry): void {
52
+ super.registerCommands(commands);
53
+ commands.registerCommand(AI_SESSIONS_NEW_SESSION, this.commandHandlerFactory({
54
+ execute: () => this.commandRegistry.executeCommand(AI_CHAT_HOME.id)
55
+ }));
56
+ }
57
+
58
+ registerToolbarItems(registry: TabBarToolbarRegistry): void {
59
+ registry.registerItem({
60
+ id: AI_SESSIONS_NEW_SESSION.id,
61
+ command: AI_SESSIONS_NEW_SESSION.id,
62
+ tooltip: nls.localizeByDefault('New Chat'),
63
+ isVisible: widget => this.activationService.isActive && widget instanceof AISessionsWidget,
64
+ when: ENABLE_AI_CONTEXT_KEY
65
+ });
66
+ }
67
+ }
@@ -0,0 +1,151 @@
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 { ChatAgentService, ChatService, ChatSessionMetadata } from '@theia/ai-chat';
18
+ import { AI_CHAT_OPEN_SESSION } from '@theia/ai-chat-ui/lib/browser/chat-view-commands';
19
+ import { formatTimeAgo } from '@theia/ai-chat-ui/lib/browser/chat-date-utils';
20
+ import { ChatSessionItemAction, ChatSessionItemActionContribution } from './chat-session-item-action-contribution';
21
+ import { ChatSessionListService } from './chat-session-list-service';
22
+ import { SessionRow, SessionsList } from './chat-session-list-components';
23
+ import { ChatSessionItem } from './chat-session-item';
24
+ import { CommandRegistry, ContributionProvider, nls } from '@theia/core';
25
+ import { codicon, HoverService, ReactWidget } from '@theia/core/lib/browser';
26
+ import { MarkdownRenderer, MarkdownRendererFactory } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
27
+ import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
28
+ import * as React from '@theia/core/shared/react';
29
+
30
+ @injectable()
31
+ export class AISessionsWidget extends ReactWidget {
32
+
33
+ static readonly ID = 'ai-sessions-widget';
34
+ static readonly LABEL = nls.localize('theia/ai-ide/sessionsView', 'AI Sessions');
35
+
36
+ @inject(ChatSessionListService)
37
+ protected readonly sessionListService: ChatSessionListService;
38
+
39
+ @inject(ChatService)
40
+ protected readonly chatService: ChatService;
41
+
42
+ @inject(ChatAgentService)
43
+ protected readonly chatAgentService: ChatAgentService;
44
+
45
+ @inject(CommandRegistry)
46
+ protected readonly commandRegistry: CommandRegistry;
47
+
48
+ @inject(HoverService)
49
+ protected readonly hoverService: HoverService;
50
+
51
+ @inject(MarkdownRendererFactory)
52
+ protected readonly markdownRendererFactory: MarkdownRendererFactory;
53
+
54
+ @inject(ContributionProvider) @named(ChatSessionItemActionContribution)
55
+ protected readonly chatSessionItemActionContributions: ContributionProvider<ChatSessionItemActionContribution>;
56
+
57
+ protected _markdownRenderer: MarkdownRenderer | undefined;
58
+ protected get markdownRenderer(): MarkdownRenderer {
59
+ if (!this._markdownRenderer) {
60
+ this._markdownRenderer = this.markdownRendererFactory();
61
+ }
62
+ return this._markdownRenderer;
63
+ }
64
+
65
+ @postConstruct()
66
+ protected init(): void {
67
+ this.id = AISessionsWidget.ID;
68
+ this.title.label = AISessionsWidget.LABEL;
69
+ this.title.caption = AISessionsWidget.LABEL;
70
+ this.title.closable = true;
71
+ this.title.iconClass = codicon('history');
72
+ this.addClass('ai-sessions-view');
73
+
74
+ this.toDispose.pushAll([
75
+ this.sessionListService.onStateChanged(() => this.update()),
76
+ this.sessionListService.onUnreadChanged(() => this.update())
77
+ ]);
78
+
79
+ this.update();
80
+ }
81
+
82
+ protected render(): React.ReactNode {
83
+ const sections = this.sessionListService.getSections();
84
+ const total = sections.active.length + sections.restored.length;
85
+
86
+ if (total === 0) {
87
+ return (
88
+ <div className="ai-sessions-view-empty">
89
+ <span className={codicon('comment-discussion')} />
90
+ <p>{nls.localize('theia/ai-ide/noSessions', 'No chat sessions yet.')}</p>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ const rows = this.sessionListService.buildRows(sections);
96
+
97
+ return (
98
+ <div className="ai-sessions-view-content">
99
+ <SessionsList
100
+ rows={rows}
101
+ renderRow={this.renderSessionRow}
102
+ />
103
+ </div>
104
+ );
105
+ }
106
+
107
+ protected getSessionActions(session: ChatSessionMetadata): ChatSessionItemAction[] {
108
+ return this.chatSessionItemActionContributions
109
+ .getContributions()
110
+ .flatMap(c => c.getActions(session))
111
+ .filter(action => this.commandRegistry.isEnabled(action.commandId, session))
112
+ .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
113
+ }
114
+
115
+ protected renderSessionRow = (row: SessionRow): React.ReactNode => this.renderSessionRowAtDepth(row, 0);
116
+
117
+ protected renderSessionRowAtDepth(row: SessionRow, depth: number): React.ReactNode {
118
+ const hasChildSessions = row.childSessions.length > 0;
119
+ const isExpanded = hasChildSessions && this.sessionListService.isExpanded(row.session.sessionId);
120
+ const descendantNeedsAttention = this.sessionListService.descendantRequiresAction(row);
121
+
122
+ return (
123
+ <React.Fragment key={row.session.sessionId}>
124
+ <ChatSessionItem
125
+ session={row.session}
126
+ isRestored={row.isRestored}
127
+ chatService={this.chatService}
128
+ chatAgentService={this.chatAgentService}
129
+ hoverService={this.hoverService}
130
+ markdownRenderer={this.markdownRenderer}
131
+ unreadState={this.sessionListService}
132
+ onClick={async () => {
133
+ await this.commandRegistry.executeCommand(AI_CHAT_OPEN_SESSION.id, row.session.sessionId);
134
+ }}
135
+ actions={this.getSessionActions(row.session)}
136
+ onAction={(action: ChatSessionItemAction, s: ChatSessionMetadata) => {
137
+ this.commandRegistry.executeCommand(action.commandId, s);
138
+ }}
139
+ formatTimeAgo={date => formatTimeAgo(date)}
140
+ hasChildSessions={hasChildSessions}
141
+ isChildSession={depth > 0}
142
+ depth={depth}
143
+ isExpanded={isExpanded}
144
+ descendantNeedsAttention={descendantNeedsAttention}
145
+ onToggleExpand={hasChildSessions ? () => this.sessionListService.toggleExpand(row.session.sessionId) : undefined}
146
+ />
147
+ {isExpanded && row.childSessions.map(child => this.renderSessionRowAtDepth(child, depth + 1))}
148
+ </React.Fragment>
149
+ );
150
+ }
151
+ }
@@ -58,13 +58,13 @@ export class DefaultChatSessionItemActionContribution implements ChatSessionItem
58
58
  {
59
59
  commandId: ChatCommands.AI_CHAT_RENAME_SESSION.id,
60
60
  iconClass: codicon('edit'),
61
- tooltip: nls.localizeByDefault('Rename Chat'),
61
+ tooltip: nls.localizeByDefault('Rename chat'),
62
62
  priority: 0,
63
63
  },
64
64
  {
65
65
  commandId: ChatCommands.AI_CHAT_DELETE_SESSION.id,
66
66
  iconClass: codicon('remove-close'),
67
- tooltip: nls.localize('theia/ai/ide/deleteChat', 'Delete Chat'),
67
+ tooltip: nls.localizeByDefault('Delete Chat'),
68
68
  priority: 10,
69
69
  },
70
70
  ];