@theia/ai-tool-sketchpad 1.74.0-next.53

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 (43) hide show
  1. package/README.md +44 -0
  2. package/lib/browser/frontend-module.d.ts +5 -0
  3. package/lib/browser/frontend-module.d.ts.map +1 -0
  4. package/lib/browser/frontend-module.js +41 -0
  5. package/lib/browser/frontend-module.js.map +1 -0
  6. package/lib/browser/index.d.ts +4 -0
  7. package/lib/browser/index.d.ts.map +1 -0
  8. package/lib/browser/index.js +22 -0
  9. package/lib/browser/index.js.map +1 -0
  10. package/lib/browser/sketched-tool-contribution.d.ts +7 -0
  11. package/lib/browser/sketched-tool-contribution.d.ts.map +1 -0
  12. package/lib/browser/sketched-tool-contribution.js +42 -0
  13. package/lib/browser/sketched-tool-contribution.js.map +1 -0
  14. package/lib/browser/sketched-tool-service.d.ts +37 -0
  15. package/lib/browser/sketched-tool-service.d.ts.map +1 -0
  16. package/lib/browser/sketched-tool-service.js +266 -0
  17. package/lib/browser/sketched-tool-service.js.map +1 -0
  18. package/lib/browser/sketched-tool-widget.d.ts +23 -0
  19. package/lib/browser/sketched-tool-widget.d.ts.map +1 -0
  20. package/lib/browser/sketched-tool-widget.js +455 -0
  21. package/lib/browser/sketched-tool-widget.js.map +1 -0
  22. package/lib/common/index.d.ts +2 -0
  23. package/lib/common/index.d.ts.map +1 -0
  24. package/lib/common/index.js +20 -0
  25. package/lib/common/index.js.map +1 -0
  26. package/lib/common/sketched-tool.d.ts +51 -0
  27. package/lib/common/sketched-tool.d.ts.map +1 -0
  28. package/lib/common/sketched-tool.js +85 -0
  29. package/lib/common/sketched-tool.js.map +1 -0
  30. package/lib/package.spec.d.ts +1 -0
  31. package/lib/package.spec.d.ts.map +1 -0
  32. package/lib/package.spec.js +24 -0
  33. package/lib/package.spec.js.map +1 -0
  34. package/package.json +52 -0
  35. package/src/browser/frontend-module.ts +43 -0
  36. package/src/browser/index.ts +19 -0
  37. package/src/browser/sketched-tool-contribution.ts +37 -0
  38. package/src/browser/sketched-tool-service.ts +282 -0
  39. package/src/browser/sketched-tool-widget.tsx +707 -0
  40. package/src/browser/style/index.css +387 -0
  41. package/src/common/index.ts +17 -0
  42. package/src/common/sketched-tool.ts +118 -0
  43. package/src/package.spec.ts +25 -0
@@ -0,0 +1,707 @@
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 { codicon, ConfirmDialog, ReactWidget } from '@theia/core/lib/browser';
18
+ import { nls } from '@theia/core';
19
+ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
20
+ import * as React from '@theia/core/shared/react';
21
+ import { generateUuid } from '@theia/core/lib/common/uuid';
22
+ import { SketchedToolDefinition, SketchedToolParameterDefinition, SketchedToolParameterType, SketchedToolReturnMode, SketchedToolService } from '../common';
23
+
24
+ const PARAMETER_TYPES: SketchedToolParameterType[] = ['string', 'number', 'integer', 'boolean', 'object', 'array'];
25
+ const ITEM_TYPES: Array<'string' | 'number' | 'integer' | 'boolean' | 'object'> = ['string', 'number', 'integer', 'boolean', 'object'];
26
+ const MAX_NESTING_DEPTH = 2;
27
+
28
+ function toolDisplayName(tool: SketchedToolDefinition): string {
29
+ return tool.name.trim() || nls.localize('theia/ai-tool-sketchpad/untitledTool', 'Untitled tool');
30
+ }
31
+
32
+ function returnModeLabel(mode: SketchedToolReturnMode): string {
33
+ return mode === 'askAtRuntime'
34
+ ? nls.localize('theia/ai-tool-sketchpad/returnModeAskShort', 'Ask at runtime')
35
+ : nls.localize('theia/ai-tool-sketchpad/returnModeStaticShort', 'Static');
36
+ }
37
+
38
+ /** Renders a tool as a call signature, e.g. `getWeather(city, units?)`. Optional parameters get a trailing `?`. */
39
+ function toolSignature(tool: SketchedToolDefinition): string {
40
+ const name = tool.name.trim() || 'tool';
41
+ const args = tool.parameters
42
+ .map(param => `${param.name.trim() || 'arg'}${param.required ? '' : '?'}`)
43
+ .join(', ');
44
+ return `${name}(${args})`;
45
+ }
46
+
47
+ function toolSummary(tool: SketchedToolDefinition): string {
48
+ const count = tool.parameters.length;
49
+ const params = count === 1
50
+ ? nls.localize('theia/ai-tool-sketchpad/oneParameter', '1 parameter')
51
+ : nls.localize('theia/ai-tool-sketchpad/nParameters', '{0} parameters', count);
52
+ return `${params} · ${returnModeLabel(tool.returnMode ?? 'static')}`;
53
+ }
54
+
55
+ interface ToolListItemProps {
56
+ tool: SketchedToolDefinition;
57
+ isSelected: boolean;
58
+ onSelect: (tool: SketchedToolDefinition) => void;
59
+ onDelete: (id: string) => void;
60
+ }
61
+
62
+ function ToolListItem({ tool, isSelected, onSelect, onDelete }: ToolListItemProps): React.ReactElement {
63
+ const handleClick = React.useCallback(() => onSelect(tool), [tool, onSelect]);
64
+ const handleDelete = React.useCallback((e: React.MouseEvent): void => {
65
+ e.stopPropagation();
66
+ onDelete(tool.id);
67
+ }, [tool.id, onDelete]);
68
+ return (
69
+ <li
70
+ className={`theia-TreeNode ai-sketchpad-list-item${isSelected ? ' theia-mod-selected' : ''}`}
71
+ onClick={handleClick}
72
+ title={toolSignature(tool)}
73
+ >
74
+ <i className={`ai-sketchpad-list-item-icon ${codicon('symbol-method')}`} />
75
+ <div className='ai-sketchpad-list-item-text'>
76
+ <span className='ai-sketchpad-list-item-label'>{toolDisplayName(tool)}</span>
77
+ <span className='ai-sketchpad-list-item-meta'>{toolSummary(tool)}</span>
78
+ </div>
79
+ <span
80
+ className={`ai-sketchpad-delete-icon ${codicon('trash')}`}
81
+ title={nls.localizeByDefault('Delete')}
82
+ onClick={handleDelete}
83
+ />
84
+ </li>
85
+ );
86
+ }
87
+
88
+ interface ToolDetailFormProps {
89
+ tool: SketchedToolDefinition;
90
+ existingTools: SketchedToolDefinition[];
91
+ onSave: (tool: SketchedToolDefinition) => void;
92
+ onDelete: (id: string) => void;
93
+ }
94
+
95
+ function ToolDetailForm({ tool: initialTool, existingTools, onSave, onDelete }: ToolDetailFormProps): React.ReactElement {
96
+ const [tool, setTool] = React.useState<SketchedToolDefinition>(deepCopy(initialTool));
97
+ const [isDirty, setIsDirty] = React.useState(false);
98
+
99
+ const nameError = SketchedToolDefinition.validateName(tool.name, existingTools, tool.id);
100
+
101
+ React.useEffect(() => {
102
+ setTool(deepCopy(initialTool));
103
+ setIsDirty(false);
104
+ }, [initialTool.id]);
105
+
106
+ const updateField = (field: 'name' | 'description' | 'staticReturn', value: string): void => {
107
+ setTool(prev => ({ ...prev, [field]: value }));
108
+ setIsDirty(true);
109
+ };
110
+
111
+ const updateReturnMode = (mode: SketchedToolReturnMode): void => {
112
+ setTool(prev => ({ ...prev, returnMode: mode }));
113
+ setIsDirty(true);
114
+ };
115
+
116
+ const getParamsAtPath = (params: SketchedToolParameterDefinition[], path: number[]): SketchedToolParameterDefinition[] | undefined => {
117
+ let current = params;
118
+ for (const index of path) {
119
+ if (index < 0 || index >= current.length) {
120
+ return undefined;
121
+ }
122
+ const param = current[index];
123
+ if (!param.properties) {
124
+ param.properties = [];
125
+ }
126
+ current = param.properties;
127
+ }
128
+ return current;
129
+ };
130
+
131
+ const getItemPropertiesAtPath = (params: SketchedToolParameterDefinition[], path: number[]): SketchedToolParameterDefinition[] | undefined => {
132
+ let current = params;
133
+ for (let i = 0; i < path.length - 1; i++) {
134
+ const index = path[i];
135
+ if (index < 0 || index >= current.length) {
136
+ return undefined;
137
+ }
138
+ const parent = current[index];
139
+ if (!parent.properties) {
140
+ parent.properties = [];
141
+ }
142
+ current = parent.properties;
143
+ }
144
+ const lastIndex = path[path.length - 1];
145
+ if (lastIndex < 0 || lastIndex >= current.length) {
146
+ return undefined;
147
+ }
148
+ const param = current[lastIndex];
149
+ if (!param.itemProperties) {
150
+ param.itemProperties = [];
151
+ }
152
+ return param.itemProperties;
153
+ };
154
+
155
+ const updateParameter = (path: number[], field: string, value: unknown): void => {
156
+ if (path.length === 0) {
157
+ return;
158
+ }
159
+ setTool(prev => {
160
+ const next = deepCopy(prev);
161
+ const parentPath = path.slice(0, -1);
162
+ const index = path[path.length - 1];
163
+ const params = getParamsAtPath(next.parameters, parentPath);
164
+ if (params && index >= 0 && index < params.length) {
165
+ const param = { ...params[index], [field]: value };
166
+ if (field === 'type' && value === 'object' && !param.properties) {
167
+ param.properties = [];
168
+ }
169
+ if (field === 'type' && value === 'array' && !param.itemType) {
170
+ param.itemType = 'string';
171
+ }
172
+ if (field === 'itemType' && value === 'object' && !param.itemProperties) {
173
+ param.itemProperties = [];
174
+ }
175
+ params[index] = param;
176
+ }
177
+ return next;
178
+ });
179
+ setIsDirty(true);
180
+ };
181
+
182
+ const handleAddParameter = (path: number[]): void => {
183
+ const newParam: SketchedToolParameterDefinition = {
184
+ name: '',
185
+ description: '',
186
+ type: 'string'
187
+ };
188
+ setTool(prev => {
189
+ const next = deepCopy(prev);
190
+ const params = getParamsAtPath(next.parameters, path);
191
+ if (params) {
192
+ params.push(newParam);
193
+ }
194
+ return next;
195
+ });
196
+ setIsDirty(true);
197
+ };
198
+
199
+ const handleAddItemProperty = (path: number[]): void => {
200
+ const newParam: SketchedToolParameterDefinition = {
201
+ name: '',
202
+ description: '',
203
+ type: 'string'
204
+ };
205
+ setTool(prev => {
206
+ const next = deepCopy(prev);
207
+ const itemProps = getItemPropertiesAtPath(next.parameters, path);
208
+ if (itemProps) {
209
+ itemProps.push(newParam);
210
+ }
211
+ return next;
212
+ });
213
+ setIsDirty(true);
214
+ };
215
+
216
+ const handleRemoveParameter = (path: number[]): void => {
217
+ if (path.length === 0) {
218
+ return;
219
+ }
220
+ setTool(prev => {
221
+ const next = deepCopy(prev);
222
+ const parentPath = path.slice(0, -1);
223
+ const index = path[path.length - 1];
224
+ const params = getParamsAtPath(next.parameters, parentPath);
225
+ if (params && index >= 0 && index < params.length) {
226
+ params.splice(index, 1);
227
+ }
228
+ return next;
229
+ });
230
+ setIsDirty(true);
231
+ };
232
+
233
+ const handleRemoveItemProperty = (paramPath: number[], propertyIndex: number): void => {
234
+ setTool(prev => {
235
+ const next = deepCopy(prev);
236
+ const itemProps = getItemPropertiesAtPath(next.parameters, paramPath);
237
+ if (itemProps && propertyIndex >= 0 && propertyIndex < itemProps.length) {
238
+ itemProps.splice(propertyIndex, 1);
239
+ }
240
+ return next;
241
+ });
242
+ setIsDirty(true);
243
+ };
244
+
245
+ const handleSave = (): void => {
246
+ if (!nameError) {
247
+ onSave({ ...tool, name: tool.name.trim() });
248
+ setIsDirty(false);
249
+ }
250
+ };
251
+
252
+ const renderParameters = (params: SketchedToolParameterDefinition[], path: number[], depth: number): React.ReactNode => {
253
+ if (params.length === 0) {
254
+ return (
255
+ <div className='ai-sketchpad-no-params'>
256
+ {nls.localize('theia/ai-tool-sketchpad/noParameters', 'No parameters defined.')}
257
+ </div>
258
+ );
259
+ }
260
+ return (
261
+ <div className='ai-sketchpad-param-list'>
262
+ {params.map((param, index) => renderParameterRow(param, [...path, index], depth))}
263
+ </div>
264
+ );
265
+ };
266
+
267
+ const renderItemProperties = (params: SketchedToolParameterDefinition[], paramPath: number[], depth: number): React.ReactNode => {
268
+ if (params.length === 0) {
269
+ return (
270
+ <div className='ai-sketchpad-no-params'>
271
+ {nls.localize('theia/ai-tool-sketchpad/noItemProperties', 'No item properties defined.')}
272
+ </div>
273
+ );
274
+ }
275
+ return (
276
+ <div className='ai-sketchpad-param-list'>
277
+ {params.map((param, index) => renderItemPropertyRow(param, paramPath, index, depth))}
278
+ </div>
279
+ );
280
+ };
281
+
282
+ const renderItemPropertyRow = (param: SketchedToolParameterDefinition, paramPath: number[], propertyIndex: number, depth: number): React.ReactNode => {
283
+ const key = `${paramPath.join('-')}-item-${propertyIndex}`;
284
+ const depthClass = depth > 0 ? ` ai-sketchpad-param-depth-${depth}` : '';
285
+ return (
286
+ <div key={key} className={`ai-sketchpad-param-item${depthClass}`}>
287
+ <div className='ai-sketchpad-param-row'>
288
+ <input
289
+ className='theia-input ai-sketchpad-param-name'
290
+ value={param.name}
291
+ placeholder={nls.localizeByDefault('Name')}
292
+ onChange={e => updateItemProperty(paramPath, propertyIndex, 'name', e.target.value)}
293
+ />
294
+ <input
295
+ className='theia-input ai-sketchpad-param-desc'
296
+ value={param.description}
297
+ placeholder={nls.localizeByDefault('Description')}
298
+ onChange={e => updateItemProperty(paramPath, propertyIndex, 'description', e.target.value)}
299
+ />
300
+ <select
301
+ className='theia-select ai-sketchpad-param-type'
302
+ value={param.type}
303
+ onChange={e => updateItemProperty(paramPath, propertyIndex, 'type', e.target.value)}
304
+ >
305
+ {ITEM_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
306
+ </select>
307
+ <label className='ai-sketchpad-param-required'>
308
+ <input
309
+ type='checkbox'
310
+ checked={param.required ?? false}
311
+ onChange={e => updateItemProperty(paramPath, propertyIndex, 'required', e.target.checked)}
312
+ />
313
+ <span>{nls.localize('theia/ai-tool-sketchpad/required', 'Required')}</span>
314
+ </label>
315
+ <span
316
+ className={`ai-sketchpad-param-delete ${codicon('close')}`}
317
+ title={nls.localizeByDefault('Remove')}
318
+ onClick={() => handleRemoveItemProperty(paramPath, propertyIndex)}
319
+ />
320
+ </div>
321
+ </div>
322
+ );
323
+ };
324
+
325
+ const updateItemProperty = (paramPath: number[], propertyIndex: number, field: string, value: unknown): void => {
326
+ setTool(prev => {
327
+ const next = deepCopy(prev);
328
+ const itemProps = getItemPropertiesAtPath(next.parameters, paramPath);
329
+ if (itemProps && propertyIndex >= 0 && propertyIndex < itemProps.length) {
330
+ itemProps[propertyIndex] = { ...itemProps[propertyIndex], [field]: value };
331
+ }
332
+ return next;
333
+ });
334
+ setIsDirty(true);
335
+ };
336
+
337
+ const renderParameterRow = (param: SketchedToolParameterDefinition, path: number[], depth: number): React.ReactNode => {
338
+ const key = path.join('-');
339
+ const depthClass = depth > 0 ? ` ai-sketchpad-param-depth-${depth}` : '';
340
+ return (
341
+ <div key={key} className={`ai-sketchpad-param-item${depthClass}`}>
342
+ <div className='ai-sketchpad-param-row'>
343
+ <input
344
+ className='theia-input ai-sketchpad-param-name'
345
+ value={param.name}
346
+ placeholder={nls.localizeByDefault('Name')}
347
+ onChange={e => updateParameter(path, 'name', e.target.value)}
348
+ />
349
+ <input
350
+ className='theia-input ai-sketchpad-param-desc'
351
+ value={param.description}
352
+ placeholder={nls.localizeByDefault('Description')}
353
+ onChange={e => updateParameter(path, 'description', e.target.value)}
354
+ />
355
+ <select
356
+ className='theia-select ai-sketchpad-param-type'
357
+ value={param.type}
358
+ onChange={e => updateParameter(path, 'type', e.target.value)}
359
+ >
360
+ {PARAMETER_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
361
+ </select>
362
+ <label className='ai-sketchpad-param-required'>
363
+ <input
364
+ type='checkbox'
365
+ checked={param.required ?? false}
366
+ onChange={e => updateParameter(path, 'required', e.target.checked)}
367
+ />
368
+ <span>{nls.localize('theia/ai-tool-sketchpad/required', 'Required')}</span>
369
+ </label>
370
+ <span
371
+ className={`ai-sketchpad-param-delete ${codicon('close')}`}
372
+ title={nls.localizeByDefault('Remove')}
373
+ onClick={() => handleRemoveParameter(path)}
374
+ />
375
+ </div>
376
+ {param.type === 'object' && depth < MAX_NESTING_DEPTH && (
377
+ <div className='ai-sketchpad-param-nested'>
378
+ {renderParameters(param.properties ?? [], path, depth + 1)}
379
+ <button
380
+ className='theia-button secondary ai-sketchpad-add-nested-btn'
381
+ onClick={() => handleAddParameter(path)}
382
+ >
383
+ <i className={codicon('add')} />&nbsp;
384
+ {nls.localize('theia/ai-tool-sketchpad/addProperty', 'Add Property')}
385
+ </button>
386
+ </div>
387
+ )}
388
+ {param.type === 'array' && (
389
+ <div className='ai-sketchpad-param-array-type'>
390
+ <label>{nls.localize('theia/ai-tool-sketchpad/itemType', 'Item Type:')}</label>
391
+ <select
392
+ className='theia-select'
393
+ value={param.itemType ?? 'string'}
394
+ onChange={e => updateParameter(path, 'itemType', e.target.value)}
395
+ >
396
+ {ITEM_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
397
+ </select>
398
+ </div>
399
+ )}
400
+ {param.type === 'array' && param.itemType === 'object' && depth < MAX_NESTING_DEPTH && (
401
+ <div className='ai-sketchpad-param-nested'>
402
+ {renderItemProperties(param.itemProperties ?? [], path, depth + 1)}
403
+ <button
404
+ className='theia-button secondary ai-sketchpad-add-nested-btn'
405
+ onClick={() => handleAddItemProperty(path)}
406
+ >
407
+ <i className={codicon('add')} />&nbsp;
408
+ {nls.localize('theia/ai-tool-sketchpad/addItemProperty', 'Add Item Property')}
409
+ </button>
410
+ </div>
411
+ )}
412
+ </div>
413
+ );
414
+ };
415
+
416
+ return (
417
+ <div className='ai-sketchpad-detail'>
418
+ <div className='ai-sketchpad-form'>
419
+ <div className='ai-sketchpad-tool-header'>
420
+ <div className='ai-sketchpad-tool-title'>
421
+ <i className={`ai-sketchpad-tool-title-icon ${codicon('symbol-method')}`} />
422
+ <span className='ai-sketchpad-tool-title-name'>{toolDisplayName(tool)}</span>
423
+ <span className='ai-sketchpad-mode-badge'>{returnModeLabel(tool.returnMode ?? 'static')}</span>
424
+ </div>
425
+ <code className='ai-sketchpad-tool-signature'>{toolSignature(tool)}</code>
426
+ </div>
427
+
428
+ <div className='ai-sketchpad-form-field'>
429
+ <label>{nls.localize('theia/ai-tool-sketchpad/toolName', 'Tool Name')}</label>
430
+ <input
431
+ className={`theia-input${isDirty && nameError ? ' ai-sketchpad-input-error' : ''}`}
432
+ value={tool.name}
433
+ placeholder={nls.localize('theia/ai-tool-sketchpad/toolNamePlaceholder', 'e.g. getWeather')}
434
+ onChange={e => updateField('name', e.target.value)}
435
+ />
436
+ {isDirty && nameError && (
437
+ <span className='ai-sketchpad-error'>{nameError}</span>
438
+ )}
439
+ </div>
440
+
441
+ <div className='ai-sketchpad-form-field'>
442
+ <label>{nls.localizeByDefault('Description')}</label>
443
+ <textarea
444
+ className='theia-input ai-sketchpad-textarea'
445
+ value={tool.description}
446
+ placeholder={nls.localize('theia/ai-tool-sketchpad/descriptionPlaceholder', 'Describe what the tool does...')}
447
+ onChange={e => updateField('description', e.target.value)}
448
+ rows={3}
449
+ />
450
+ </div>
451
+
452
+ <div className='ai-sketchpad-form-field'>
453
+ <label>{nls.localize('theia/ai-tool-sketchpad/parameters', 'Parameters')}</label>
454
+ {renderParameters(tool.parameters, [], 0)}
455
+ <button className='theia-button secondary ai-sketchpad-add-param-btn' onClick={() => handleAddParameter([])}>
456
+ <i className={codicon('add')} />&nbsp;
457
+ {nls.localize('theia/ai-tool-sketchpad/addParameter', 'Add Parameter')}
458
+ </button>
459
+ </div>
460
+
461
+ <div className='ai-sketchpad-form-field'>
462
+ <label>{nls.localize('theia/ai-tool-sketchpad/returnMode', 'Return Mode')}</label>
463
+ <select
464
+ className='theia-select'
465
+ value={tool.returnMode ?? 'static'}
466
+ onChange={e => updateReturnMode(e.target.value as SketchedToolReturnMode)}
467
+ >
468
+ <option value='static'>
469
+ {nls.localize('theia/ai-tool-sketchpad/returnModeStatic', 'Static Return Value')}
470
+ </option>
471
+ <option value='askAtRuntime'>
472
+ {nls.localize('theia/ai-tool-sketchpad/returnModeAskAtRuntime', 'Ask At Runtime')}
473
+ </option>
474
+ </select>
475
+ </div>
476
+
477
+ {tool.returnMode !== 'askAtRuntime' && (
478
+ <div className='ai-sketchpad-form-field'>
479
+ <label>{nls.localize('theia/ai-tool-sketchpad/staticReturn', 'Static Return Value')}</label>
480
+ <textarea
481
+ className='theia-input ai-sketchpad-textarea ai-sketchpad-textarea-code'
482
+ value={tool.staticReturn}
483
+ placeholder={nls.localize('theia/ai-tool-sketchpad/staticReturnPlaceholder',
484
+ 'The value returned when this tool is invoked (e.g. JSON string)')}
485
+ onChange={e => updateField('staticReturn', e.target.value)}
486
+ rows={4}
487
+ />
488
+ <span className='ai-sketchpad-hint'>
489
+ {nls.localize('theia/ai-tool-sketchpad/staticReturnHint',
490
+ 'This static value is returned every time the tool is called, regardless of the input arguments.')}
491
+ </span>
492
+ </div>
493
+ )}
494
+
495
+ {tool.returnMode === 'askAtRuntime' && (
496
+ <div className='ai-sketchpad-form-field'>
497
+ <span className='ai-sketchpad-hint'>
498
+ {nls.localize('theia/ai-tool-sketchpad/askAtRuntimeHint',
499
+ 'When the LLM calls this tool, a quick input will open asking you to provide the return value.')}
500
+ </span>
501
+ </div>
502
+ )}
503
+
504
+ <div className='ai-sketchpad-actions'>
505
+ <button className='theia-button secondary' onClick={() => onDelete(tool.id)}>
506
+ {nls.localizeByDefault('Delete')}
507
+ </button>
508
+ <button
509
+ className='theia-button main'
510
+ onClick={handleSave}
511
+ disabled={!isDirty || !!nameError}
512
+ >
513
+ {nls.localizeByDefault('Save')}
514
+ </button>
515
+ </div>
516
+ </div>
517
+ </div>
518
+ );
519
+ }
520
+
521
+ function deepCopy<T>(obj: T): T {
522
+ return JSON.parse(JSON.stringify(obj));
523
+ }
524
+
525
+ @injectable()
526
+ export class SketchedToolWidget extends ReactWidget {
527
+
528
+ static readonly ID = 'ai-tool-sketchpad';
529
+ static readonly LABEL = nls.localize('theia/ai-tool-sketchpad/label', 'AI Tool Sketchpad');
530
+
531
+ @inject(SketchedToolService)
532
+ protected readonly sketchedToolService: SketchedToolService;
533
+
534
+ protected tools: SketchedToolDefinition[] = [];
535
+ protected selectedToolId: string | undefined;
536
+ protected editingTool: SketchedToolDefinition | undefined;
537
+ protected isNewUnsavedTool = false;
538
+
539
+ @postConstruct()
540
+ protected init(): void {
541
+ this.id = SketchedToolWidget.ID;
542
+ this.title.label = SketchedToolWidget.LABEL;
543
+ this.title.caption = SketchedToolWidget.LABEL;
544
+ this.title.closable = true;
545
+ this.title.iconClass = codicon('beaker');
546
+ this.addClass('ai-sketchpad-widget');
547
+
548
+ this.loadTools();
549
+
550
+ this.toDispose.push(
551
+ this.sketchedToolService.onDidChangeSketchedTools(() => {
552
+ this.loadTools();
553
+ })
554
+ );
555
+ }
556
+
557
+ protected loadTools(): void {
558
+ this.tools = this.sketchedToolService.getSketchedTools();
559
+ if (this.selectedToolId) {
560
+ const stillExists = this.tools.find(t => t.id === this.selectedToolId);
561
+ if (!stillExists) {
562
+ // Preserve a new tool that hasn't been saved to the service yet
563
+ if (!this.isNewUnsavedTool) {
564
+ this.selectedToolId = undefined;
565
+ this.editingTool = undefined;
566
+ }
567
+ } else {
568
+ this.isNewUnsavedTool = false;
569
+ this.editingTool = deepCopy(stillExists);
570
+ }
571
+ }
572
+ this.update();
573
+ }
574
+
575
+ protected render(): React.ReactNode {
576
+ return (
577
+ <div className='ai-sketchpad-container'>
578
+ {this.renderList()}
579
+ {this.renderDetail()}
580
+ </div>
581
+ );
582
+ }
583
+
584
+ protected renderList(): React.ReactNode {
585
+ return (
586
+ <div className='ai-sketchpad-list theia-TreeContainer'>
587
+ {this.tools.length === 0 ? (
588
+ <div className='ai-sketchpad-list-empty'>
589
+ {nls.localize('theia/ai-tool-sketchpad/noTools', 'No tools yet.')}
590
+ </div>
591
+ ) : (
592
+ <ul>
593
+ {this.tools.map(tool => (
594
+ <ToolListItem
595
+ key={tool.id}
596
+ tool={tool}
597
+ isSelected={this.selectedToolId === tool.id}
598
+ onSelect={this.handleSelectTool}
599
+ onDelete={this.handleDeleteTool}
600
+ />
601
+ ))}
602
+ </ul>
603
+ )}
604
+ <div className='ai-sketchpad-add-button'>
605
+ <button className='theia-button main' onClick={this.handleAddTool}>
606
+ <i className={codicon('add')} />&nbsp;
607
+ {nls.localize('theia/ai-tool-sketchpad/addTool', 'Add Tool')}
608
+ </button>
609
+ </div>
610
+ </div>
611
+ );
612
+ }
613
+
614
+ protected renderDetail(): React.ReactNode {
615
+ if (!this.editingTool) {
616
+ return (
617
+ <div className='ai-sketchpad-detail'>
618
+ <div className='ai-sketchpad-empty-state'>
619
+ <i className={`ai-sketchpad-empty-icon ${codicon('beaker')}`} />
620
+ <span className='ai-sketchpad-empty-message'>
621
+ {nls.localize('theia/ai-tool-sketchpad/selectTool', 'Select a tool to edit, or add a new one to start sketching.')}
622
+ </span>
623
+ </div>
624
+ </div>
625
+ );
626
+ }
627
+
628
+ return (
629
+ <ToolDetailForm
630
+ key={this.editingTool.id}
631
+ tool={this.editingTool}
632
+ existingTools={this.tools}
633
+ onSave={this.handleSave}
634
+ onDelete={this.handleDeleteTool}
635
+ />
636
+ );
637
+ }
638
+
639
+ // --- Event Handlers (arrow functions per coding guidelines) ---
640
+
641
+ protected handleSelectTool = (tool: SketchedToolDefinition): void => {
642
+ this.selectedToolId = tool.id;
643
+ this.editingTool = deepCopy(tool);
644
+ this.update();
645
+ };
646
+
647
+ protected handleAddTool = (): void => {
648
+ const newTool: SketchedToolDefinition = {
649
+ id: generateUuid(),
650
+ name: '',
651
+ description: '',
652
+ parameters: [],
653
+ returnMode: 'static',
654
+ staticReturn: ''
655
+ };
656
+ this.selectedToolId = newTool.id;
657
+ this.editingTool = newTool;
658
+ this.isNewUnsavedTool = true;
659
+ this.update();
660
+ };
661
+
662
+ protected handleDeleteTool = async (toolId: string): Promise<void> => {
663
+ const existing = this.tools.find(t => t.id === toolId);
664
+ if (existing) {
665
+ const confirmed = await new ConfirmDialog({
666
+ title: nls.localizeByDefault('Delete'),
667
+ msg: nls.localize('theia/ai-tool-sketchpad/confirmDelete',
668
+ 'Are you sure you want to delete the tool "{0}"?', existing.name || existing.id),
669
+ ok: nls.localizeByDefault('Delete'),
670
+ cancel: nls.localizeByDefault('Cancel')
671
+ }).open();
672
+ if (!confirmed) {
673
+ return;
674
+ }
675
+ this.clearSelectionIfMatches(toolId);
676
+ await this.sketchedToolService.removeSketchedTool(toolId);
677
+ // loadTools() will be triggered via onDidChangeSketchedTools and call update()
678
+ } else {
679
+ // New unsaved tool not yet in the service — just discard and refresh the UI
680
+ this.clearSelectionIfMatches(toolId);
681
+ this.update();
682
+ }
683
+ };
684
+
685
+ protected clearSelectionIfMatches(toolId: string): void {
686
+ if (this.selectedToolId === toolId) {
687
+ this.selectedToolId = undefined;
688
+ this.editingTool = undefined;
689
+ this.isNewUnsavedTool = false;
690
+ }
691
+ }
692
+
693
+ protected handleSave = async (tool: SketchedToolDefinition): Promise<void> => {
694
+ if (SketchedToolDefinition.validateName(tool.name, this.tools, tool.id)) {
695
+ return;
696
+ }
697
+ this.editingTool = tool;
698
+ this.isNewUnsavedTool = false;
699
+ const existing = this.tools.find(t => t.id === tool.id);
700
+ if (existing) {
701
+ await this.sketchedToolService.updateSketchedTool(tool);
702
+ } else {
703
+ await this.sketchedToolService.addSketchedTool(tool);
704
+ }
705
+ // loadTools() is triggered via onDidChangeSketchedTools and calls update()
706
+ };
707
+ }