@theia/ai-chat 1.63.0-next.0 → 1.63.0-next.52
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.
- package/lib/browser/agent-delegation-tool.d.ts +25 -0
- package/lib/browser/agent-delegation-tool.d.ts.map +1 -0
- package/lib/browser/agent-delegation-tool.js +171 -0
- package/lib/browser/agent-delegation-tool.js.map +1 -0
- package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-frontend-module.js +15 -1
- package/lib/browser/ai-chat-frontend-module.js.map +1 -1
- package/lib/browser/change-set-file-element.d.ts +28 -7
- package/lib/browser/change-set-file-element.d.ts.map +1 -1
- package/lib/browser/change-set-file-element.js +86 -18
- package/lib/browser/change-set-file-element.js.map +1 -1
- package/lib/browser/change-set-variable.js +1 -2
- package/lib/browser/change-set-variable.js.map +1 -1
- package/lib/browser/chat-tool-preferences.d.ts +54 -0
- package/lib/browser/chat-tool-preferences.d.ts.map +1 -0
- package/lib/browser/chat-tool-preferences.js +170 -0
- package/lib/browser/chat-tool-preferences.js.map +1 -0
- package/lib/browser/chat-tool-request-service.d.ts +20 -0
- package/lib/browser/chat-tool-request-service.d.ts.map +1 -0
- package/lib/browser/chat-tool-request-service.js +89 -0
- package/lib/browser/chat-tool-request-service.js.map +1 -0
- package/lib/browser/delegation-response-content.d.ts +20 -0
- package/lib/browser/delegation-response-content.d.ts.map +1 -0
- package/lib/browser/delegation-response-content.js +51 -0
- package/lib/browser/delegation-response-content.js.map +1 -0
- package/lib/browser/file-chat-variable-contribution.d.ts +15 -1
- package/lib/browser/file-chat-variable-contribution.d.ts.map +1 -1
- package/lib/browser/file-chat-variable-contribution.js +111 -5
- package/lib/browser/file-chat-variable-contribution.js.map +1 -1
- package/lib/browser/frontend-chat-service.d.ts.map +1 -1
- package/lib/browser/frontend-chat-service.js +2 -6
- package/lib/browser/frontend-chat-service.js.map +1 -1
- package/lib/browser/image-context-variable-contribution.d.ts +27 -0
- package/lib/browser/image-context-variable-contribution.d.ts.map +1 -0
- package/lib/browser/image-context-variable-contribution.js +149 -0
- package/lib/browser/image-context-variable-contribution.js.map +1 -0
- package/lib/browser/task-context-service.d.ts +9 -3
- package/lib/browser/task-context-service.d.ts.map +1 -1
- package/lib/browser/task-context-service.js +111 -9
- package/lib/browser/task-context-service.js.map +1 -1
- package/lib/browser/task-context-storage-service.d.ts +1 -0
- package/lib/browser/task-context-storage-service.d.ts.map +1 -1
- package/lib/browser/task-context-storage-service.js +4 -1
- package/lib/browser/task-context-storage-service.js.map +1 -1
- package/lib/common/change-set.d.ts +78 -0
- package/lib/common/change-set.d.ts.map +1 -0
- package/lib/common/change-set.js +133 -0
- package/lib/common/change-set.js.map +1 -0
- package/lib/common/chat-agent-service.d.ts +1 -0
- package/lib/common/chat-agent-service.d.ts.map +1 -1
- package/lib/common/chat-agent-service.js +2 -1
- package/lib/common/chat-agent-service.js.map +1 -1
- package/lib/common/chat-agents.d.ts +2 -2
- package/lib/common/chat-agents.d.ts.map +1 -1
- package/lib/common/chat-agents.js +25 -6
- package/lib/common/chat-agents.js.map +1 -1
- package/lib/common/chat-model.d.ts +68 -80
- package/lib/common/chat-model.d.ts.map +1 -1
- package/lib/common/chat-model.js +224 -136
- package/lib/common/chat-model.js.map +1 -1
- package/lib/common/chat-request-parser.d.ts.map +1 -1
- package/lib/common/chat-request-parser.js +3 -0
- package/lib/common/chat-request-parser.js.map +1 -1
- package/lib/common/chat-service.d.ts +6 -5
- package/lib/common/chat-service.d.ts.map +1 -1
- package/lib/common/chat-service.js +9 -11
- package/lib/common/chat-service.js.map +1 -1
- package/lib/common/image-context-variable.d.ts +29 -0
- package/lib/common/image-context-variable.d.ts.map +1 -0
- package/lib/common/image-context-variable.js +99 -0
- package/lib/common/image-context-variable.js.map +1 -0
- package/package.json +10 -9
- package/src/browser/agent-delegation-tool.ts +207 -0
- package/src/browser/ai-chat-frontend-module.ts +28 -3
- package/src/browser/change-set-file-element.ts +97 -25
- package/src/browser/change-set-variable.ts +1 -1
- package/src/browser/chat-tool-preferences.ts +178 -0
- package/src/browser/chat-tool-request-service.ts +93 -0
- package/src/browser/delegation-response-content.ts +55 -0
- package/src/browser/file-chat-variable-contribution.ts +120 -6
- package/src/browser/frontend-chat-service.ts +3 -6
- package/src/browser/image-context-variable-contribution.ts +153 -0
- package/src/browser/task-context-service.ts +115 -9
- package/src/browser/task-context-storage-service.ts +5 -1
- package/src/common/change-set.ts +197 -0
- package/src/common/chat-agent-service.ts +1 -0
- package/src/common/chat-agents.ts +40 -19
- package/src/common/chat-model.ts +258 -208
- package/src/common/chat-request-parser.ts +3 -0
- package/src/common/chat-service.ts +11 -13
- package/src/common/image-context-variable.ts +116 -0
|
@@ -0,0 +1,178 @@
|
|
|
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 { AI_CORE_PREFERENCES_TITLE } from '@theia/ai-core/lib/browser/ai-core-preferences';
|
|
18
|
+
import { nls } from '@theia/core';
|
|
19
|
+
import { interfaces, injectable, inject } from '@theia/core/shared/inversify';
|
|
20
|
+
import {
|
|
21
|
+
createPreferenceProxy,
|
|
22
|
+
PreferenceProxy,
|
|
23
|
+
PreferenceService,
|
|
24
|
+
PreferenceSchema,
|
|
25
|
+
PreferenceContribution
|
|
26
|
+
} from '@theia/core/lib/browser/preferences';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Enum for tool confirmation modes
|
|
30
|
+
*/
|
|
31
|
+
export enum ToolConfirmationMode {
|
|
32
|
+
ALWAYS_ALLOW = 'always_allow',
|
|
33
|
+
CONFIRM = 'confirm',
|
|
34
|
+
DISABLED = 'disabled'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const TOOL_CONFIRMATION_PREFERENCE = 'ai-features.chat.toolConfirmation';
|
|
38
|
+
|
|
39
|
+
export const chatToolPreferences: PreferenceSchema = {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
[TOOL_CONFIRMATION_PREFERENCE]: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
additionalProperties: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
enum: [ToolConfirmationMode.ALWAYS_ALLOW, ToolConfirmationMode.CONFIRM, ToolConfirmationMode.DISABLED],
|
|
47
|
+
enumDescriptions: [
|
|
48
|
+
nls.localize('theia/ai/chat/toolConfirmation/yolo/description', 'Execute tools automatically without confirmation'),
|
|
49
|
+
nls.localize('theia/ai/chat/toolConfirmation/confirm/description', 'Ask for confirmation before executing tools'),
|
|
50
|
+
nls.localize('theia/ai/chat/toolConfirmation/disabled/description', 'Disable tool execution')
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
default: {},
|
|
54
|
+
description: nls.localize('theia/ai/chat/toolConfirmation/description',
|
|
55
|
+
'Configure confirmation behavior for different tools. Key is the tool ID, value is the confirmation mode.' +
|
|
56
|
+
'Use "*" as the key to set a global default for all tools.'),
|
|
57
|
+
title: AI_CORE_PREFERENCES_TITLE,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export interface ChatToolConfiguration {
|
|
63
|
+
[TOOL_CONFIRMATION_PREFERENCE]: { [toolId: string]: ToolConfirmationMode };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const ChatToolPreferenceContribution = Symbol('ChatToolPreferenceContribution');
|
|
67
|
+
export const ChatToolPreferences = Symbol('ChatToolPreferences');
|
|
68
|
+
export type ChatToolPreferences = PreferenceProxy<ChatToolConfiguration>;
|
|
69
|
+
|
|
70
|
+
export function createChatToolPreferences(preferences: PreferenceService, schema: PreferenceSchema = chatToolPreferences): ChatToolPreferences {
|
|
71
|
+
return createPreferenceProxy(preferences, schema);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function bindChatToolPreferences(bind: interfaces.Bind): void {
|
|
75
|
+
bind(ChatToolPreferences).toDynamicValue((ctx: interfaces.Context) => {
|
|
76
|
+
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
|
|
77
|
+
const contribution = ctx.container.get<PreferenceContribution>(ChatToolPreferenceContribution);
|
|
78
|
+
return createChatToolPreferences(preferences, contribution.schema);
|
|
79
|
+
}).inSingletonScope();
|
|
80
|
+
bind(ChatToolPreferenceContribution).toConstantValue({ schema: chatToolPreferences });
|
|
81
|
+
bind(PreferenceContribution).toService(ChatToolPreferenceContribution);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Utility class to manage tool confirmation settings
|
|
86
|
+
*/
|
|
87
|
+
@injectable()
|
|
88
|
+
export class ToolConfirmationManager {
|
|
89
|
+
@inject(ChatToolPreferences)
|
|
90
|
+
protected readonly preferences: ChatToolPreferences;
|
|
91
|
+
|
|
92
|
+
@inject(PreferenceService)
|
|
93
|
+
protected readonly preferenceService: PreferenceService;
|
|
94
|
+
|
|
95
|
+
// In-memory session overrides (not persisted), per chat
|
|
96
|
+
protected sessionOverrides: Map<string, Map<string, ToolConfirmationMode>> = new Map();
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the confirmation mode for a specific tool, considering session overrides first (per chat)
|
|
100
|
+
*/
|
|
101
|
+
getConfirmationMode(toolId: string, chatId: string): ToolConfirmationMode {
|
|
102
|
+
const chatMap = this.sessionOverrides.get(chatId);
|
|
103
|
+
if (chatMap && chatMap.has(toolId)) {
|
|
104
|
+
return chatMap.get(toolId)!;
|
|
105
|
+
}
|
|
106
|
+
const toolConfirmation = this.preferences[TOOL_CONFIRMATION_PREFERENCE];
|
|
107
|
+
if (toolConfirmation[toolId]) {
|
|
108
|
+
return toolConfirmation[toolId];
|
|
109
|
+
}
|
|
110
|
+
if (toolConfirmation['*']) {
|
|
111
|
+
return toolConfirmation['*'];
|
|
112
|
+
}
|
|
113
|
+
return ToolConfirmationMode.ALWAYS_ALLOW; // Default to Always Allow
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Set the confirmation mode for a specific tool (persisted)
|
|
118
|
+
*/
|
|
119
|
+
setConfirmationMode(toolId: string, mode: ToolConfirmationMode): void {
|
|
120
|
+
const current = this.preferences[TOOL_CONFIRMATION_PREFERENCE] || {};
|
|
121
|
+
// Determine the global default (star entry), or fallback to schema default
|
|
122
|
+
let starMode = current['*'];
|
|
123
|
+
if (starMode === undefined) {
|
|
124
|
+
starMode = ToolConfirmationMode.ALWAYS_ALLOW;
|
|
125
|
+
}
|
|
126
|
+
if (mode === starMode) {
|
|
127
|
+
// Remove the toolId entry if it exists
|
|
128
|
+
if (toolId in current) {
|
|
129
|
+
const { [toolId]: _, ...rest } = current;
|
|
130
|
+
this.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, rest);
|
|
131
|
+
}
|
|
132
|
+
// else, nothing to update
|
|
133
|
+
} else {
|
|
134
|
+
// Set or update the toolId entry
|
|
135
|
+
const updated = { ...current, [toolId]: mode };
|
|
136
|
+
this.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, updated);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Set the confirmation mode for a specific tool for this session only (not persisted, per chat)
|
|
142
|
+
*/
|
|
143
|
+
setSessionConfirmationMode(toolId: string, mode: ToolConfirmationMode, chatId: string): void {
|
|
144
|
+
let chatMap = this.sessionOverrides.get(chatId);
|
|
145
|
+
if (!chatMap) {
|
|
146
|
+
chatMap = new Map();
|
|
147
|
+
this.sessionOverrides.set(chatId, chatMap);
|
|
148
|
+
}
|
|
149
|
+
chatMap.set(toolId, mode);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clear all session overrides for a specific chat, or all if no chatId is given
|
|
154
|
+
*/
|
|
155
|
+
clearSessionOverrides(chatId?: string): void {
|
|
156
|
+
if (chatId) {
|
|
157
|
+
this.sessionOverrides.delete(chatId);
|
|
158
|
+
} else {
|
|
159
|
+
this.sessionOverrides.clear();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get all tool confirmation settings
|
|
165
|
+
*/
|
|
166
|
+
getAllConfirmationSettings(): { [toolId: string]: ToolConfirmationMode } {
|
|
167
|
+
return this.preferences[TOOL_CONFIRMATION_PREFERENCE] || {};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
resetAllConfirmationModeSettings(): void {
|
|
171
|
+
const current = this.preferences[TOOL_CONFIRMATION_PREFERENCE] || {};
|
|
172
|
+
if ('*' in current) {
|
|
173
|
+
this.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, { '*': current['*'] });
|
|
174
|
+
} else {
|
|
175
|
+
this.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, {});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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 { ToolRequest } from '@theia/ai-core';
|
|
18
|
+
import { injectable, inject } from '@theia/core/shared/inversify';
|
|
19
|
+
import { ChatToolRequestService, ChatToolRequest } from '../common/chat-tool-request-service';
|
|
20
|
+
import { MutableChatRequestModel, ToolCallChatResponseContent } from '../common/chat-model';
|
|
21
|
+
import { ToolConfirmationManager, ToolConfirmationMode, ChatToolPreferences } from './chat-tool-preferences';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Frontend-specific implementation of ChatToolRequestService that handles tool confirmation
|
|
25
|
+
*/
|
|
26
|
+
@injectable()
|
|
27
|
+
export class FrontendChatToolRequestService extends ChatToolRequestService {
|
|
28
|
+
|
|
29
|
+
@inject(ToolConfirmationManager)
|
|
30
|
+
protected readonly confirmationManager: ToolConfirmationManager;
|
|
31
|
+
|
|
32
|
+
@inject(ChatToolPreferences)
|
|
33
|
+
protected readonly preferences: ChatToolPreferences;
|
|
34
|
+
|
|
35
|
+
protected override toChatToolRequest(toolRequest: ToolRequest, request: MutableChatRequestModel): ChatToolRequest {
|
|
36
|
+
const confirmationMode = this.confirmationManager.getConfirmationMode(toolRequest.id, request.session.id);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
...toolRequest,
|
|
40
|
+
handler: async (arg_string: string) => {
|
|
41
|
+
switch (confirmationMode) {
|
|
42
|
+
case ToolConfirmationMode.DISABLED:
|
|
43
|
+
return { denied: true, message: `Tool ${toolRequest.id} is disabled` };
|
|
44
|
+
|
|
45
|
+
case ToolConfirmationMode.ALWAYS_ALLOW:
|
|
46
|
+
// Execute immediately without confirmation
|
|
47
|
+
return toolRequest.handler(arg_string, request);
|
|
48
|
+
|
|
49
|
+
case ToolConfirmationMode.CONFIRM:
|
|
50
|
+
default:
|
|
51
|
+
// Create confirmation requirement
|
|
52
|
+
const toolCallContent = this.findToolCallContent(toolRequest, arg_string, request);
|
|
53
|
+
const confirmed = await toolCallContent.confirmed;
|
|
54
|
+
|
|
55
|
+
if (confirmed) {
|
|
56
|
+
return toolRequest.handler(arg_string, request);
|
|
57
|
+
} else {
|
|
58
|
+
// Return an object indicating the user denied the tool execution
|
|
59
|
+
// instead of throwing an error
|
|
60
|
+
return { denied: true, message: `Tool execution denied by user: ${toolRequest.id}` };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Find existing tool call content or create a new one for confirmation tracking
|
|
69
|
+
*
|
|
70
|
+
* Looks for ToolCallChatResponseContent nodes where the name field matches the toolRequest id.
|
|
71
|
+
* Starts from the back of the content array to find the most recent match.
|
|
72
|
+
*/
|
|
73
|
+
protected findToolCallContent(
|
|
74
|
+
toolRequest: ToolRequest,
|
|
75
|
+
arguments_: string,
|
|
76
|
+
request: MutableChatRequestModel
|
|
77
|
+
): ToolCallChatResponseContent {
|
|
78
|
+
// Look for existing tool call content with matching ID
|
|
79
|
+
const response = request.response.response;
|
|
80
|
+
const contentArray = response.content;
|
|
81
|
+
|
|
82
|
+
// Start from the end of the array and find the first match
|
|
83
|
+
for (let i = contentArray.length - 1; i >= 0; i--) {
|
|
84
|
+
const content = contentArray[i];
|
|
85
|
+
if (ToolCallChatResponseContent.is(content) &&
|
|
86
|
+
content.name === toolRequest.id) {
|
|
87
|
+
return content;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new Error(`Tool call content for tool ${toolRequest.id} not found in the response`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
import { isObject } from '@theia/core';
|
|
17
|
+
import { ChatRequestInvocation, ChatResponseContent } from '../common';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Response Content created when an Agent delegates a prompt to another agent.
|
|
21
|
+
* Contains agent id, delegated prompt, and the response.
|
|
22
|
+
*/
|
|
23
|
+
export class DelegationResponseContent implements ChatResponseContent {
|
|
24
|
+
kind = 'AgentDelegation';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param agentId The id of the agent to whom the task was delegated
|
|
28
|
+
* @param prompt The prompt that was delegated
|
|
29
|
+
* @param response The response from the delegated agent
|
|
30
|
+
*/
|
|
31
|
+
constructor(
|
|
32
|
+
public agentId: string,
|
|
33
|
+
public prompt: string,
|
|
34
|
+
public response: ChatRequestInvocation
|
|
35
|
+
) {
|
|
36
|
+
// Empty
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
asString(): string {
|
|
40
|
+
const json = {
|
|
41
|
+
agentId: this.agentId,
|
|
42
|
+
prompt: this.prompt
|
|
43
|
+
};
|
|
44
|
+
return JSON.stringify(json);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isDelegationResponseContent(
|
|
49
|
+
value: unknown
|
|
50
|
+
): value is DelegationResponseContent {
|
|
51
|
+
return (
|
|
52
|
+
isObject<DelegationResponseContent>(value) &&
|
|
53
|
+
value.kind === 'AgentDelegation'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
import { AIVariableContext, AIVariableResolutionRequest, PromptText } from '@theia/ai-core';
|
|
18
18
|
import { AIVariableCompletionContext, AIVariableDropResult, FrontendVariableContribution, FrontendVariableService } from '@theia/ai-core/lib/browser';
|
|
19
19
|
import { FILE_VARIABLE } from '@theia/ai-core/lib/browser/file-variable-contribution';
|
|
20
|
-
import { CancellationToken, QuickInputService, URI } from '@theia/core';
|
|
20
|
+
import { CancellationToken, ILogger, QuickInputService, URI } from '@theia/core';
|
|
21
21
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
22
22
|
import * as monaco from '@theia/monaco-editor-core';
|
|
23
23
|
import { FileQuickPickItem, QuickFileSelectService } from '@theia/file-search/lib/browser/quick-file-select-service';
|
|
24
24
|
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
25
25
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
26
26
|
import { VARIABLE_ADD_CONTEXT_COMMAND } from './ai-chat-frontend-contribution';
|
|
27
|
+
import { IMAGE_CONTEXT_VARIABLE, ImageContextVariable } from '../common/image-context-variable';
|
|
27
28
|
|
|
28
29
|
@injectable()
|
|
29
30
|
export class FileChatVariableContribution implements FrontendVariableContribution {
|
|
@@ -39,8 +40,12 @@ export class FileChatVariableContribution implements FrontendVariableContributio
|
|
|
39
40
|
@inject(QuickFileSelectService)
|
|
40
41
|
protected readonly quickFileSelectService: QuickFileSelectService;
|
|
41
42
|
|
|
43
|
+
@inject(ILogger)
|
|
44
|
+
protected readonly logger: ILogger;
|
|
45
|
+
|
|
42
46
|
registerVariables(service: FrontendVariableService): void {
|
|
43
47
|
service.registerArgumentPicker(FILE_VARIABLE, this.triggerArgumentPicker.bind(this));
|
|
48
|
+
service.registerArgumentPicker(IMAGE_CONTEXT_VARIABLE, this.imageArgumentPicker.bind(this));
|
|
44
49
|
service.registerArgumentCompletionProvider(FILE_VARIABLE, this.provideArgumentCompletionItems.bind(this));
|
|
45
50
|
service.registerDropHandler(this.handleDrop.bind(this));
|
|
46
51
|
}
|
|
@@ -68,6 +73,57 @@ export class FileChatVariableContribution implements FrontendVariableContributio
|
|
|
68
73
|
});
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
protected async imageArgumentPicker(): Promise<string | undefined> {
|
|
77
|
+
const quickPick = this.quickInputService.createQuickPick();
|
|
78
|
+
quickPick.title = 'Select an image file';
|
|
79
|
+
|
|
80
|
+
// Get all files and filter only image files
|
|
81
|
+
const allPicks = await this.quickFileSelectService.getPicks();
|
|
82
|
+
quickPick.items = allPicks.filter(item => {
|
|
83
|
+
if (FileQuickPickItem.is(item)) {
|
|
84
|
+
return this.isImageFile(item.uri.path.toString());
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const updateItems = async (value: string) => {
|
|
90
|
+
const filteredPicks = await this.quickFileSelectService.getPicks(value, CancellationToken.None);
|
|
91
|
+
quickPick.items = filteredPicks.filter(item => {
|
|
92
|
+
if (FileQuickPickItem.is(item)) {
|
|
93
|
+
return this.isImageFile(item.uri.path.toString());
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const onChangeListener = quickPick.onDidChangeValue(updateItems);
|
|
100
|
+
quickPick.show();
|
|
101
|
+
|
|
102
|
+
return new Promise(resolve => {
|
|
103
|
+
quickPick.onDispose(onChangeListener.dispose);
|
|
104
|
+
quickPick.onDidAccept(async () => {
|
|
105
|
+
const selectedItem = quickPick.selectedItems[0];
|
|
106
|
+
if (selectedItem && FileQuickPickItem.is(selectedItem)) {
|
|
107
|
+
quickPick.dispose();
|
|
108
|
+
const filePath = await this.wsService.getWorkspaceRelativePath(selectedItem.uri);
|
|
109
|
+
const fileName = selectedItem.uri.displayName;
|
|
110
|
+
const base64Data = await this.fileToBase64(selectedItem.uri);
|
|
111
|
+
const mimeType = this.getMimeTypeFromExtension(selectedItem.uri.path.toString());
|
|
112
|
+
|
|
113
|
+
// Create the argument string in the required format
|
|
114
|
+
const imageVarArgs: ImageContextVariable = {
|
|
115
|
+
name: fileName,
|
|
116
|
+
wsRelativePath: filePath,
|
|
117
|
+
data: base64Data,
|
|
118
|
+
mimeType: mimeType
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
resolve(ImageContextVariable.createArgString(imageVarArgs));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
71
127
|
protected async provideArgumentCompletionItems(
|
|
72
128
|
model: monaco.editor.ITextModel,
|
|
73
129
|
position: monaco.Position,
|
|
@@ -106,6 +162,50 @@ export class FileChatVariableContribution implements FrontendVariableContributio
|
|
|
106
162
|
);
|
|
107
163
|
}
|
|
108
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Checks if a file is an image based on its extension.
|
|
167
|
+
*/
|
|
168
|
+
protected isImageFile(filePath: string): boolean {
|
|
169
|
+
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.webp'];
|
|
170
|
+
const extension = filePath.toLowerCase().substring(filePath.lastIndexOf('.'));
|
|
171
|
+
return imageExtensions.includes(extension);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Determines the MIME type based on file extension.
|
|
176
|
+
*/
|
|
177
|
+
protected getMimeTypeFromExtension(filePath: string): string {
|
|
178
|
+
const extension = filePath.toLowerCase().substring(filePath.lastIndexOf('.'));
|
|
179
|
+
const mimeTypes: { [key: string]: string } = {
|
|
180
|
+
'.jpg': 'image/jpeg',
|
|
181
|
+
'.jpeg': 'image/jpeg',
|
|
182
|
+
'.png': 'image/png',
|
|
183
|
+
'.gif': 'image/gif',
|
|
184
|
+
'.bmp': 'image/bmp',
|
|
185
|
+
'.svg': 'image/svg+xml',
|
|
186
|
+
'.webp': 'image/webp'
|
|
187
|
+
};
|
|
188
|
+
return mimeTypes[extension] || 'application/octet-stream';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Converts a file to base64 data URL.
|
|
193
|
+
*/
|
|
194
|
+
protected async fileToBase64(uri: URI): Promise<string> {
|
|
195
|
+
try {
|
|
196
|
+
const fileContent = await this.fileService.readFile(uri);
|
|
197
|
+
const uint8Array = new Uint8Array(fileContent.value.buffer);
|
|
198
|
+
let binary = '';
|
|
199
|
+
for (let i = 0; i < uint8Array.length; i++) {
|
|
200
|
+
binary += String.fromCharCode(uint8Array[i]);
|
|
201
|
+
}
|
|
202
|
+
return btoa(binary);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
this.logger.error('Error reading file content:', error);
|
|
205
|
+
return '';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
109
209
|
protected async handleDrop(event: DragEvent, _: AIVariableContext): Promise<AIVariableDropResult | undefined> {
|
|
110
210
|
const data = event.dataTransfer?.getData('selected-tree-nodes');
|
|
111
211
|
if (!data) {
|
|
@@ -126,11 +226,25 @@ export class FileChatVariableContribution implements FrontendVariableContributio
|
|
|
126
226
|
const uri = URI.fromFilePath(filePath);
|
|
127
227
|
if (await this.fileService.exists(uri)) {
|
|
128
228
|
const wsRelativePath = await this.wsService.getWorkspaceRelativePath(uri);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
229
|
+
const fileName = uri.displayName;
|
|
230
|
+
|
|
231
|
+
if (this.isImageFile(filePath)) {
|
|
232
|
+
const base64Data = await this.fileToBase64(uri);
|
|
233
|
+
const mimeType = this.getMimeTypeFromExtension(filePath);
|
|
234
|
+
variables.push(ImageContextVariable.createRequest({
|
|
235
|
+
[ImageContextVariable.name]: fileName,
|
|
236
|
+
[ImageContextVariable.wsRelativePath]: wsRelativePath,
|
|
237
|
+
[ImageContextVariable.data]: base64Data,
|
|
238
|
+
[ImageContextVariable.mimeType]: mimeType
|
|
239
|
+
}));
|
|
240
|
+
// we do not want to push a text for image variables
|
|
241
|
+
} else {
|
|
242
|
+
variables.push({
|
|
243
|
+
variable: FILE_VARIABLE,
|
|
244
|
+
arg: wsRelativePath
|
|
245
|
+
});
|
|
246
|
+
texts.push(`${PromptText.VARIABLE_CHAR}${FILE_VARIABLE.name}${PromptText.VARIABLE_SEPARATOR_CHAR}${wsRelativePath}`);
|
|
247
|
+
}
|
|
134
248
|
}
|
|
135
249
|
}
|
|
136
250
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
18
|
-
import {
|
|
18
|
+
import { ChatAgent, ChatAgentLocation, ChatChangeEvent, ChatServiceImpl, ChatSession, ParsedChatRequest, SessionOptions } from '../common';
|
|
19
19
|
import { PreferenceService } from '@theia/core/lib/browser';
|
|
20
20
|
import { DEFAULT_CHAT_AGENT_PREF, PIN_CHAT_AGENT_PREF } from './ai-chat-preferences';
|
|
21
21
|
import { ChangeSetFileService } from './change-set-file-service';
|
|
@@ -68,11 +68,8 @@ export class FrontendChatServiceImpl extends ChatServiceImpl {
|
|
|
68
68
|
override createSession(location?: ChatAgentLocation, options?: SessionOptions, pinnedAgent?: ChatAgent): ChatSession {
|
|
69
69
|
const session = super.createSession(location, options, pinnedAgent);
|
|
70
70
|
session.model.onDidChange(event => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.changeSetFileService.closeDiffsForSession(session.id);
|
|
74
|
-
} else if (changeSet) {
|
|
75
|
-
this.changeSetFileService.closeDiffsForSession(session.id, changeSet.getElements().map(({ uri }) => uri));
|
|
71
|
+
if (ChatChangeEvent.isChangeSetEvent(event)) {
|
|
72
|
+
this.changeSetFileService.closeDiffsForSession(session.id, session.model.changeSet.getElements().map(({ uri }) => uri));
|
|
76
73
|
}
|
|
77
74
|
});
|
|
78
75
|
return session;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource GmbH 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 {
|
|
18
|
+
AIVariableContext, AIVariableContribution,
|
|
19
|
+
AIVariableOpener, AIVariableResolutionRequest, AIVariableResolver, ResolvedAIContextVariable
|
|
20
|
+
} from '@theia/ai-core';
|
|
21
|
+
import { FrontendVariableService, AIVariablePasteResult } from '@theia/ai-core/lib/browser';
|
|
22
|
+
import { Path, URI } from '@theia/core';
|
|
23
|
+
import { LabelProvider, LabelProviderContribution, open, OpenerService } from '@theia/core/lib/browser';
|
|
24
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
25
|
+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
26
|
+
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
27
|
+
import { IMAGE_CONTEXT_VARIABLE, ImageContextVariable, ImageContextVariableRequest } from '../common/image-context-variable';
|
|
28
|
+
|
|
29
|
+
@injectable()
|
|
30
|
+
export class ImageContextVariableContribution implements AIVariableContribution, AIVariableResolver, AIVariableOpener, LabelProviderContribution {
|
|
31
|
+
@inject(FileService)
|
|
32
|
+
protected readonly fileService: FileService;
|
|
33
|
+
|
|
34
|
+
@inject(WorkspaceService)
|
|
35
|
+
protected readonly wsService: WorkspaceService;
|
|
36
|
+
|
|
37
|
+
@inject(OpenerService)
|
|
38
|
+
protected readonly openerService: OpenerService;
|
|
39
|
+
|
|
40
|
+
@inject(LabelProvider)
|
|
41
|
+
protected readonly labelProvider: LabelProvider;
|
|
42
|
+
|
|
43
|
+
registerVariables(service: FrontendVariableService): void {
|
|
44
|
+
service.registerResolver(IMAGE_CONTEXT_VARIABLE, this);
|
|
45
|
+
service.registerOpener(IMAGE_CONTEXT_VARIABLE, this);
|
|
46
|
+
service.registerPasteHandler(this.handlePaste.bind(this));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async canResolve(request: AIVariableResolutionRequest, _: AIVariableContext): Promise<number> {
|
|
50
|
+
return ImageContextVariable.isImageContextRequest(request) ? 1 : 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async resolve(request: AIVariableResolutionRequest, _: AIVariableContext): Promise<ResolvedAIContextVariable | undefined> {
|
|
54
|
+
return ImageContextVariable.resolve(request as ImageContextVariableRequest);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async canOpen(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<number> {
|
|
58
|
+
return ImageContextVariable.isImageContextRequest(request) && !!ImageContextVariable.parseRequest(request)?.wsRelativePath ? 1 : 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async open(request: ImageContextVariableRequest, context: AIVariableContext): Promise<void> {
|
|
62
|
+
const uri = await this.toUri(request);
|
|
63
|
+
if (!uri) {
|
|
64
|
+
throw new Error('Unable to resolve URI for request.');
|
|
65
|
+
}
|
|
66
|
+
await open(this.openerService, uri);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected async toUri(request: ImageContextVariableRequest): Promise<URI | undefined> {
|
|
70
|
+
const variable = ImageContextVariable.parseRequest(request);
|
|
71
|
+
return variable?.wsRelativePath ? this.makeAbsolute(variable.wsRelativePath) : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async handlePaste(event: ClipboardEvent, context: AIVariableContext): Promise<AIVariablePasteResult | undefined> {
|
|
75
|
+
if (!event.clipboardData?.items) { return undefined; }
|
|
76
|
+
|
|
77
|
+
const variables: AIVariableResolutionRequest[] = [];
|
|
78
|
+
|
|
79
|
+
for (const item of event.clipboardData.items) {
|
|
80
|
+
if (item.type.startsWith('image/')) {
|
|
81
|
+
const blob = item.getAsFile();
|
|
82
|
+
if (blob) {
|
|
83
|
+
try {
|
|
84
|
+
const dataUrl = await this.readFileAsDataURL(blob);
|
|
85
|
+
// Extract the base64 data by removing the data URL prefix
|
|
86
|
+
// Format is like: data:image/png;base64,BASE64DATA
|
|
87
|
+
const imageData = dataUrl.substring(dataUrl.indexOf(',') + 1);
|
|
88
|
+
variables.push(ImageContextVariable.createRequest({
|
|
89
|
+
data: imageData,
|
|
90
|
+
name: blob.name || `pasted-image-${Date.now()}.png`,
|
|
91
|
+
mimeType: blob.type
|
|
92
|
+
}));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Failed to process pasted image:', error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return variables.length > 0 ? { variables } : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private readFileAsDataURL(blob: Blob): Promise<string> {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const reader = new FileReader();
|
|
106
|
+
reader.onload = e => {
|
|
107
|
+
if (!e.target?.result) {
|
|
108
|
+
reject(new Error('Failed to read file as data URL'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
resolve(e.target.result as string);
|
|
112
|
+
};
|
|
113
|
+
reader.onerror = () => reject(reader.error);
|
|
114
|
+
reader.readAsDataURL(blob);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
protected async makeAbsolute(pathStr: string): Promise<URI | undefined> {
|
|
119
|
+
const path = new Path(Path.normalizePathSeparator(pathStr));
|
|
120
|
+
if (!path.isAbsolute) {
|
|
121
|
+
const workspaceRoots = this.wsService.tryGetRoots();
|
|
122
|
+
const wsUris = workspaceRoots.map(root => root.resource.resolve(path));
|
|
123
|
+
for (const uri of wsUris) {
|
|
124
|
+
if (await this.fileService.exists(uri)) {
|
|
125
|
+
return uri;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const argUri = new URI(pathStr);
|
|
130
|
+
if (await this.fileService.exists(argUri)) {
|
|
131
|
+
return argUri;
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
canHandle(element: object): number {
|
|
137
|
+
return ImageContextVariable.isImageContextRequest(element) ? 10 : -1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getIcon(element: ImageContextVariableRequest): string | undefined {
|
|
141
|
+
const path = ImageContextVariable.parseArg(element.arg).wsRelativePath;
|
|
142
|
+
return path ? this.labelProvider.getIcon(new URI(path)) : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
getName(element: ImageContextVariableRequest): string | undefined {
|
|
146
|
+
return ImageContextVariable.parseArg(element.arg).name;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getDetails(element: ImageContextVariableRequest): string | undefined {
|
|
150
|
+
const path = ImageContextVariable.parseArg(element.arg).wsRelativePath;
|
|
151
|
+
return path ? this.labelProvider.getDetails(new URI(path)) : '[pasted]';
|
|
152
|
+
}
|
|
153
|
+
}
|