@theia/ai-codex 1.67.0-next.56

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.
@@ -0,0 +1,61 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 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 { ChatAgent } from '@theia/ai-chat';
18
+ import { ChatResponsePartRenderer } from '@theia/ai-chat-ui/lib/browser/chat-response-part-renderer';
19
+ import { Agent } from '@theia/ai-core';
20
+ import { PreferenceContribution } from '@theia/core';
21
+ import { RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser';
22
+ import { ContainerModule } from '@theia/core/shared/inversify';
23
+ import {
24
+ CODEX_SERVICE_PATH,
25
+ CodexClient,
26
+ CodexService
27
+ } from '../common/codex-service';
28
+ import { CodexPreferencesSchema } from '../common/codex-preferences';
29
+ import { CodexChatAgent } from './codex-chat-agent';
30
+ import { CodexClientImpl, CodexFrontendService } from './codex-frontend-service';
31
+ import { CommandExecutionRenderer } from './renderers/command-execution-renderer';
32
+ import { TodoListRenderer } from './renderers/todo-list-renderer';
33
+ import { WebSearchRenderer } from './renderers/web-search-renderer';
34
+ import '../../src/browser/style/codex-tool-renderers.css';
35
+
36
+ export default new ContainerModule(bind => {
37
+ bind(PreferenceContribution).toConstantValue({ schema: CodexPreferencesSchema });
38
+
39
+ bind(CodexFrontendService).toSelf().inSingletonScope();
40
+ bind(CodexClientImpl).toSelf().inSingletonScope();
41
+ bind(CodexClient).toService(CodexClientImpl);
42
+
43
+ bind(CodexService).toDynamicValue(ctx => {
44
+ const connection = ctx.container.get<ServiceConnectionProvider>(RemoteConnectionProvider);
45
+ const backendClient: CodexClient = ctx.container.get(CodexClient);
46
+ return connection.createProxy(CODEX_SERVICE_PATH, backendClient);
47
+ }).inSingletonScope();
48
+
49
+ bind(CodexChatAgent).toSelf().inSingletonScope();
50
+ bind(Agent).toService(CodexChatAgent);
51
+ bind(ChatAgent).toService(CodexChatAgent);
52
+
53
+ bind(CommandExecutionRenderer).toSelf().inSingletonScope();
54
+ bind(ChatResponsePartRenderer).toService(CommandExecutionRenderer);
55
+
56
+ bind(TodoListRenderer).toSelf().inSingletonScope();
57
+ bind(ChatResponsePartRenderer).toService(TodoListRenderer);
58
+
59
+ bind(WebSearchRenderer).toSelf().inSingletonScope();
60
+ bind(ChatResponsePartRenderer).toService(WebSearchRenderer);
61
+ });
@@ -0,0 +1,245 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
18
+ let disableJSDOM = enableJSDOM();
19
+ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
20
+ FrontendApplicationConfigProvider.set({});
21
+ import * as path from 'path';
22
+
23
+ import { expect } from 'chai';
24
+ import * as sinon from 'sinon';
25
+ import { Container, interfaces } from '@theia/core/shared/inversify';
26
+ import { PreferenceService } from '@theia/core';
27
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
28
+ import { URI } from '@theia/core/lib/common/uri';
29
+ import { CODEX_API_KEY_PREF, CodexService, CodexBackendRequest } from '../common';
30
+ import { API_KEY_PREF } from '@theia/ai-openai/lib/common/openai-preferences';
31
+
32
+ import type { CodexFrontendService, CodexClientImpl } from './codex-frontend-service';
33
+
34
+ disableJSDOM();
35
+
36
+ describe('CodexFrontendService', () => {
37
+ let container: Container;
38
+ let CodexFrontendServiceConstructor: interfaces.Newable<CodexFrontendService>;
39
+ let CodexClientImplConstructor: interfaces.Newable<CodexClientImpl>;
40
+ let mockPreferenceService: PreferenceService;
41
+ let mockBackendService: CodexService;
42
+
43
+ before(async () => {
44
+ disableJSDOM = enableJSDOM();
45
+
46
+ const serviceModule = await import('./codex-frontend-service');
47
+ CodexFrontendServiceConstructor = serviceModule.CodexFrontendService;
48
+ CodexClientImplConstructor = serviceModule.CodexClientImpl;
49
+ });
50
+
51
+ beforeEach(() => {
52
+ container = new Container();
53
+
54
+ mockPreferenceService = {
55
+ get: sinon.stub()
56
+ } as unknown as PreferenceService;
57
+
58
+ mockBackendService = {
59
+ send: sinon.stub<[CodexBackendRequest, string], Promise<void>>().resolves(),
60
+ cancel: sinon.stub<[string], void>()
61
+ };
62
+
63
+ const mockWorkspaceService = {
64
+ roots: Promise.resolve([{ resource: new URI('file:///test/workspace') }])
65
+ } as WorkspaceService;
66
+
67
+ container.bind(PreferenceService).toConstantValue(mockPreferenceService);
68
+ container.bind(CodexService).toConstantValue(mockBackendService);
69
+ container.bind(WorkspaceService).toConstantValue(mockWorkspaceService);
70
+ container.bind(CodexClientImplConstructor).toSelf().inSingletonScope();
71
+ container.bind(CodexFrontendServiceConstructor).toSelf();
72
+ });
73
+
74
+ afterEach(() => {
75
+ sinon.restore();
76
+ });
77
+
78
+ after(() => {
79
+ disableJSDOM();
80
+ });
81
+
82
+ describe('API Key Preference Hierarchy', () => {
83
+ it('should prioritize Codex-specific API key over shared OpenAI key', async () => {
84
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(CODEX_API_KEY_PREF).returns('codex-key-123');
85
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(API_KEY_PREF).returns('openai-key-456');
86
+
87
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
88
+ await service.send({ prompt: 'test', sessionId: 'session-1' });
89
+
90
+ expect((mockBackendService.send as sinon.SinonStub).calledOnce).to.be.true;
91
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
92
+ expect(backendRequest.apiKey).to.equal('codex-key-123');
93
+ });
94
+
95
+ it('should fall back to shared OpenAI API key when Codex key not set', async () => {
96
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(CODEX_API_KEY_PREF).returns(undefined);
97
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(API_KEY_PREF).returns('openai-key-456');
98
+
99
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
100
+ await service.send({ prompt: 'test', sessionId: 'session-1' });
101
+
102
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
103
+ expect(backendRequest.apiKey).to.equal('openai-key-456');
104
+ });
105
+
106
+ it('should return undefined when neither key is set', async () => {
107
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(CODEX_API_KEY_PREF).returns(undefined);
108
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(API_KEY_PREF).returns(undefined);
109
+
110
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
111
+ await service.send({ prompt: 'test', sessionId: 'session-1' });
112
+
113
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
114
+ expect(backendRequest.apiKey).to.be.undefined;
115
+ });
116
+
117
+ it('should treat empty Codex key as not set', async () => {
118
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(CODEX_API_KEY_PREF).returns('');
119
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(API_KEY_PREF).returns('openai-key-456');
120
+
121
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
122
+ await service.send({ prompt: 'test', sessionId: 'session-1' });
123
+
124
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
125
+ expect(backendRequest.apiKey).to.equal('openai-key-456');
126
+ });
127
+
128
+ it('should treat whitespace-only Codex key as not set', async () => {
129
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(CODEX_API_KEY_PREF).returns(' ');
130
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(API_KEY_PREF).returns('openai-key-456');
131
+
132
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
133
+ await service.send({ prompt: 'test', sessionId: 'session-1' });
134
+
135
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
136
+ expect(backendRequest.apiKey).to.equal('openai-key-456');
137
+ });
138
+
139
+ it('should treat empty OpenAI key as not set', async () => {
140
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(CODEX_API_KEY_PREF).returns(undefined);
141
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(API_KEY_PREF).returns('');
142
+
143
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
144
+ await service.send({ prompt: 'test', sessionId: 'session-1' });
145
+
146
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
147
+ expect(backendRequest.apiKey).to.be.undefined;
148
+ });
149
+
150
+ it('should treat whitespace-only OpenAI key as not set', async () => {
151
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(CODEX_API_KEY_PREF).returns(undefined);
152
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(API_KEY_PREF).returns(' ');
153
+
154
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
155
+ await service.send({ prompt: 'test', sessionId: 'session-1' });
156
+
157
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
158
+ expect(backendRequest.apiKey).to.be.undefined;
159
+ });
160
+ });
161
+
162
+ describe('Sandbox Mode Configuration', () => {
163
+ beforeEach(() => {
164
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(CODEX_API_KEY_PREF).returns('test-key');
165
+ });
166
+
167
+ it('should default to workspace-write when no sandboxMode is provided in request', async () => {
168
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
169
+ await service.send({ prompt: 'test', sessionId: 'session-1' });
170
+
171
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
172
+ expect(backendRequest.options?.sandboxMode).to.equal('workspace-write');
173
+ });
174
+
175
+ it('should use sandboxMode from request when provided', async () => {
176
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
177
+ await service.send({ prompt: 'test', sessionId: 'session-1', sandboxMode: 'read-only' });
178
+
179
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
180
+ expect(backendRequest.options?.sandboxMode).to.equal('read-only');
181
+ });
182
+
183
+ it('should use danger-full-access when provided in request', async () => {
184
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
185
+ await service.send({ prompt: 'test', sessionId: 'session-1', sandboxMode: 'danger-full-access' });
186
+
187
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
188
+ expect(backendRequest.options?.sandboxMode).to.equal('danger-full-access');
189
+ });
190
+
191
+ it('should default to workspace-write when request sandboxMode is undefined', async () => {
192
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
193
+ await service.send({ prompt: 'test', sessionId: 'session-1', sandboxMode: undefined });
194
+
195
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
196
+ expect(backendRequest.options?.sandboxMode).to.equal('workspace-write');
197
+ });
198
+ });
199
+
200
+ describe('Request Building', () => {
201
+ beforeEach(() => {
202
+ (mockPreferenceService.get as sinon.SinonStub).withArgs(CODEX_API_KEY_PREF).returns('test-key');
203
+ });
204
+
205
+ it('should include workspace root in request', async () => {
206
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
207
+ await service.send({ prompt: 'test prompt', sessionId: 'session-1' });
208
+
209
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
210
+ const expectedPath = path.join('test', 'workspace');
211
+ expect(backendRequest.options?.workingDirectory).to.include(expectedPath);
212
+ });
213
+
214
+ it('should pass prompt to backend', async () => {
215
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
216
+ await service.send({ prompt: 'my test prompt', sessionId: 'session-1' });
217
+
218
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
219
+ expect(backendRequest.prompt).to.equal('my test prompt');
220
+ });
221
+
222
+ it('should pass sessionId to backend', async () => {
223
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
224
+ await service.send({ prompt: 'test', sessionId: 'my-session-123' });
225
+
226
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
227
+ expect(backendRequest.sessionId).to.equal('my-session-123');
228
+ });
229
+
230
+ it('should merge custom options with defaults', async () => {
231
+ const service = container.get<CodexFrontendService>(CodexFrontendServiceConstructor);
232
+ await service.send({
233
+ prompt: 'test',
234
+ sessionId: 'session-1',
235
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
236
+ options: { customOption: 'value' } as any // Testing dynamic options extension
237
+ });
238
+
239
+ const backendRequest = (mockBackendService.send as sinon.SinonStub).firstCall.args[0];
240
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
241
+ expect((backendRequest.options as any)?.customOption).to.equal('value'); // Accessing dynamic test property
242
+ expect(backendRequest.options?.sandboxMode).to.equal('workspace-write');
243
+ });
244
+ });
245
+ });
@@ -0,0 +1,221 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 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 { inject, injectable } from '@theia/core/shared/inversify';
18
+ import { CancellationToken, generateUuid, PreferenceService } from '@theia/core';
19
+ import { FileUri } from '@theia/core/lib/common/file-uri';
20
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
21
+ import { API_KEY_PREF } from '@theia/ai-openai/lib/common/openai-preferences';
22
+ import type { ThreadEvent } from '@openai/codex-sdk';
23
+ import {
24
+ CodexClient,
25
+ CodexRequest,
26
+ CodexService,
27
+ CodexBackendRequest,
28
+ CODEX_API_KEY_PREF
29
+ } from '../common';
30
+
31
+ @injectable()
32
+ export class CodexClientImpl implements CodexClient {
33
+ protected tokenHandlers = new Map<string, (token?: ThreadEvent) => void>();
34
+ protected errorHandlers = new Map<string, (error: Error) => void>();
35
+
36
+ /**
37
+ * `undefined` token signals end of stream per RPC protocol.
38
+ */
39
+ sendToken(streamId: string, token?: ThreadEvent): void {
40
+ const handler = this.tokenHandlers.get(streamId);
41
+ if (handler) {
42
+ handler(token);
43
+ }
44
+ }
45
+
46
+ sendError(streamId: string, error: Error): void {
47
+ const handler = this.errorHandlers.get(streamId);
48
+ if (handler) {
49
+ handler(error);
50
+ }
51
+ }
52
+
53
+ registerTokenHandler(streamId: string, handler: (token?: ThreadEvent) => void): void {
54
+ this.tokenHandlers.set(streamId, handler);
55
+ }
56
+
57
+ registerErrorHandler(streamId: string, handler: (error: Error) => void): void {
58
+ this.errorHandlers.set(streamId, handler);
59
+ }
60
+
61
+ unregisterHandlers(streamId: string): void {
62
+ this.tokenHandlers.delete(streamId);
63
+ this.errorHandlers.delete(streamId);
64
+ }
65
+ }
66
+
67
+ interface StreamState {
68
+ id: string;
69
+ tokens: (ThreadEvent | undefined)[];
70
+ isComplete: boolean;
71
+ hasError: boolean;
72
+ error?: Error;
73
+ pendingResolve?: () => void;
74
+ pendingReject?: (error: Error) => void;
75
+ }
76
+
77
+ @injectable()
78
+ export class CodexFrontendService {
79
+
80
+ @inject(CodexService)
81
+ protected readonly backendService: CodexService;
82
+
83
+ @inject(CodexClientImpl)
84
+ protected readonly client: CodexClientImpl;
85
+
86
+ @inject(PreferenceService)
87
+ protected readonly preferenceService: PreferenceService;
88
+
89
+ @inject(WorkspaceService)
90
+ protected readonly workspaceService: WorkspaceService;
91
+
92
+ protected streams = new Map<string, StreamState>();
93
+
94
+ async send(request: CodexRequest, cancellationToken?: CancellationToken): Promise<AsyncIterable<ThreadEvent>> {
95
+ const streamState: StreamState = {
96
+ id: this.generateStreamId(),
97
+ tokens: [],
98
+ isComplete: false,
99
+ hasError: false
100
+ };
101
+ this.streams.set(streamState.id, streamState);
102
+ this.setupStreamHandlers(streamState);
103
+
104
+ cancellationToken?.onCancellationRequested(() => {
105
+ this.backendService.cancel(streamState.id);
106
+ this.cleanup(streamState.id);
107
+ });
108
+
109
+ const apiKey = this.getApiKey();
110
+ const sandboxMode = request.sandboxMode ?? 'workspace-write';
111
+ const workingDirectory = await this.getWorkspaceRoot();
112
+
113
+ const backendRequest: CodexBackendRequest = {
114
+ prompt: request.prompt,
115
+ options: {
116
+ workingDirectory,
117
+ ...request.options,
118
+ sandboxMode
119
+ },
120
+ apiKey,
121
+ sessionId: request.sessionId
122
+ };
123
+
124
+ await this.backendService.send(backendRequest, streamState.id);
125
+
126
+ return this.createAsyncIterable(streamState);
127
+ }
128
+
129
+ protected generateStreamId(): string {
130
+ return generateUuid();
131
+ }
132
+
133
+ protected setupStreamHandlers(streamState: StreamState): void {
134
+ this.client.registerTokenHandler(streamState.id, (token?: ThreadEvent) => {
135
+ if (token === undefined) {
136
+ streamState.isComplete = true;
137
+ } else {
138
+ streamState.tokens.push(token);
139
+ }
140
+
141
+ if (streamState.pendingResolve) {
142
+ streamState.pendingResolve();
143
+ streamState.pendingResolve = undefined;
144
+ }
145
+ });
146
+
147
+ this.client.registerErrorHandler(streamState.id, (error: Error) => {
148
+ streamState.hasError = true;
149
+ streamState.error = error;
150
+
151
+ if (streamState.pendingReject) {
152
+ streamState.pendingReject(error);
153
+ streamState.pendingReject = undefined;
154
+ }
155
+ });
156
+ }
157
+
158
+ protected async *createAsyncIterable(streamState: StreamState): AsyncIterable<ThreadEvent> {
159
+ let currentIndex = 0;
160
+
161
+ while (true) {
162
+ if (currentIndex < streamState.tokens.length) {
163
+ const token = streamState.tokens[currentIndex];
164
+ currentIndex++;
165
+ if (token !== undefined) {
166
+ yield token;
167
+ }
168
+ continue;
169
+ }
170
+
171
+ if (streamState.isComplete) {
172
+ break;
173
+ }
174
+
175
+ if (streamState.hasError && streamState.error) {
176
+ this.cleanup(streamState.id);
177
+ throw streamState.error;
178
+ }
179
+
180
+ await new Promise<void>((resolve, reject) => {
181
+ streamState.pendingResolve = resolve;
182
+ streamState.pendingReject = reject;
183
+ });
184
+ }
185
+
186
+ this.cleanup(streamState.id);
187
+ }
188
+
189
+ protected cleanup(streamId: string): void {
190
+ this.client.unregisterHandlers(streamId);
191
+ this.streams.delete(streamId);
192
+ }
193
+
194
+ /**
195
+ * Fallback hierarchy:
196
+ * 1. Codex-specific API key (highest priority)
197
+ * 2. Shared OpenAI API key
198
+ * 3. undefined (backend will check OPENAI_API_KEY env var)
199
+ */
200
+ protected getApiKey(): string | undefined {
201
+ const codexKey = this.preferenceService.get<string>(CODEX_API_KEY_PREF);
202
+ if (codexKey && codexKey.trim()) {
203
+ return codexKey;
204
+ }
205
+
206
+ const openaiKey = this.preferenceService.get<string>(API_KEY_PREF);
207
+ if (openaiKey && openaiKey.trim()) {
208
+ return openaiKey;
209
+ }
210
+
211
+ return undefined;
212
+ }
213
+
214
+ protected async getWorkspaceRoot(): Promise<string | undefined> {
215
+ const roots = await this.workspaceService.roots;
216
+ if (roots && roots.length > 0) {
217
+ return FileUri.fsPath(roots[0].resource.toString());
218
+ }
219
+ return undefined;
220
+ }
221
+ }
@@ -0,0 +1,42 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 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 { ToolCallChatResponseContentImpl } from '@theia/ai-chat/lib/common';
18
+ import { ToolCallResult } from '@theia/ai-core';
19
+
20
+ export class CodexToolCallChatResponseContent extends ToolCallChatResponseContentImpl {
21
+ static readonly type = 'codex-tool-call';
22
+
23
+ constructor(id?: string, name?: string, arg_string?: string, finished?: boolean, result?: ToolCallResult) {
24
+ super(id, name, arg_string, finished, result);
25
+ }
26
+
27
+ static is(content: unknown): content is CodexToolCallChatResponseContent {
28
+ return content instanceof CodexToolCallChatResponseContent;
29
+ }
30
+
31
+ update(args?: string, finished?: boolean, result?: ToolCallResult): void {
32
+ if (args !== undefined) {
33
+ this._arguments = args;
34
+ }
35
+ if (finished !== undefined) {
36
+ this._finished = finished;
37
+ }
38
+ if (result !== undefined) {
39
+ this._result = result;
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,75 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 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 { codicon } from '@theia/core/lib/browser';
18
+ import * as React from '@theia/core/shared/react';
19
+ import { ReactNode } from '@theia/core/shared/react';
20
+
21
+ interface CollapsibleToolRendererProps {
22
+ compactHeader: ReactNode;
23
+ expandedContent?: ReactNode;
24
+ onHeaderClick?: () => void;
25
+ headerStyle?: React.CSSProperties;
26
+ defaultExpanded?: boolean;
27
+ }
28
+
29
+ export const CollapsibleToolRenderer: React.FC<CollapsibleToolRendererProps> = ({
30
+ compactHeader,
31
+ expandedContent,
32
+ onHeaderClick,
33
+ headerStyle,
34
+ defaultExpanded = false
35
+ }) => {
36
+ const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);
37
+
38
+ const hasExpandableContent = expandedContent !== undefined;
39
+
40
+ const handleHeaderClick = (event: React.MouseEvent) => {
41
+ const target = event.target as HTMLElement;
42
+ if (target.closest('.clickable-element')) {
43
+ onHeaderClick?.();
44
+ return;
45
+ }
46
+
47
+ if (hasExpandableContent) {
48
+ setIsExpanded(!isExpanded);
49
+ }
50
+ onHeaderClick?.();
51
+ };
52
+
53
+ return (
54
+ <div className="codex-tool container">
55
+ <div
56
+ className={`codex-tool header${hasExpandableContent ? ' expandable' : ''}`}
57
+ onClick={handleHeaderClick}
58
+ style={{
59
+ cursor: hasExpandableContent || onHeaderClick ? 'pointer' : 'default',
60
+ ...headerStyle
61
+ }}
62
+ >
63
+ {hasExpandableContent && (
64
+ <span className={`${codicon(isExpanded ? 'chevron-down' : 'chevron-right')} codex-tool expand-icon`} />
65
+ )}
66
+ {compactHeader}
67
+ </div>
68
+ {hasExpandableContent && isExpanded && (
69
+ <div className="codex-tool expanded-content">
70
+ {expandedContent}
71
+ </div>
72
+ )}
73
+ </div>
74
+ );
75
+ };