@kispace-io/extension-ai-system 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/package.json +20 -0
  2. package/src/agents/agent-registry.ts +65 -0
  3. package/src/agents/index.ts +4 -0
  4. package/src/agents/message-processor.ts +50 -0
  5. package/src/agents/prompt-builder.ts +167 -0
  6. package/src/ai-system-extension.ts +104 -0
  7. package/src/aisystem.json +154 -0
  8. package/src/chat-provider-contributions.ts +95 -0
  9. package/src/core/constants.ts +23 -0
  10. package/src/core/index.ts +6 -0
  11. package/src/core/interfaces.ts +137 -0
  12. package/src/core/types.ts +126 -0
  13. package/src/general-assistant-prompt.txt +14 -0
  14. package/src/i18n.json +11 -0
  15. package/src/index.ts +13 -0
  16. package/src/prompt-enhancer-contributions.ts +29 -0
  17. package/src/providers/index.ts +5 -0
  18. package/src/providers/ollama-provider.ts +13 -0
  19. package/src/providers/openai-provider.ts +12 -0
  20. package/src/providers/provider-factory.ts +36 -0
  21. package/src/providers/provider.ts +156 -0
  22. package/src/providers/streaming/ollama-parser.ts +114 -0
  23. package/src/providers/streaming/sse-parser.ts +152 -0
  24. package/src/providers/streaming/stream-parser.ts +16 -0
  25. package/src/register.ts +16 -0
  26. package/src/service/ai-service.ts +744 -0
  27. package/src/service/token-usage-tracker.ts +139 -0
  28. package/src/tools/index.ts +4 -0
  29. package/src/tools/tool-call-accumulator.ts +81 -0
  30. package/src/tools/tool-executor.ts +174 -0
  31. package/src/tools/tool-registry.ts +70 -0
  32. package/src/translation.ts +3 -0
  33. package/src/utils/token-estimator.ts +87 -0
  34. package/src/utils/tool-detector.ts +144 -0
  35. package/src/view/agent-group-manager.ts +146 -0
  36. package/src/view/components/ai-agent-response-card.ts +198 -0
  37. package/src/view/components/ai-agent-response-group.ts +220 -0
  38. package/src/view/components/ai-chat-input.ts +131 -0
  39. package/src/view/components/ai-chat-message.ts +615 -0
  40. package/src/view/components/ai-empty-state.ts +52 -0
  41. package/src/view/components/ai-loading-indicator.ts +91 -0
  42. package/src/view/components/index.ts +7 -0
  43. package/src/view/components/k-ai-config-editor.ts +828 -0
  44. package/src/view/index.ts +6 -0
  45. package/src/view/k-aiview.ts +901 -0
  46. package/src/view/k-token-usage.ts +220 -0
  47. package/src/view/provider-manager.ts +196 -0
  48. package/src/view/session-manager.ts +255 -0
  49. package/src/view/stream-manager.ts +123 -0
  50. package/src/workflows/conditional-workflow.ts +98 -0
  51. package/src/workflows/index.ts +6 -0
  52. package/src/workflows/parallel-workflow.ts +45 -0
  53. package/src/workflows/sequential-workflow.ts +95 -0
  54. package/src/workflows/workflow-engine.ts +63 -0
  55. package/src/workflows/workflow-strategy.ts +21 -0
  56. package/tsconfig.json +12 -0
@@ -0,0 +1,828 @@
1
+ import { css, html, TemplateResult } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import { repeat } from 'lit/directives/repeat.js';
4
+ import { when } from 'lit/directives/when.js';
5
+ import { KPart } from '@kispace-io/core';
6
+ import { EditorInput } from '@kispace-io/core';
7
+ import { appSettings, TOPIC_SETTINGS_CHANGED } from '@kispace-io/core';
8
+ import { KEY_AI_CONFIG, TOPIC_AICONFIG_CHANGED, CID_CHAT_PROVIDERS } from '../../core/constants';
9
+ import { subscribe } from '@kispace-io/core';
10
+ import { confirmDialog } from '@kispace-io/core';
11
+ import { contributionRegistry, commandRegistry } from '@kispace-io/core';
12
+ import { ProviderFactory } from '../../providers/provider-factory';
13
+ import type { AIConfig, ChatProvider } from '../../core/types';
14
+ import type { ChatProviderContribution } from '../../core/interfaces';
15
+ import { t } from '../../translation';
16
+
17
+ @customElement('k-ai-config-editor')
18
+ export class KAIConfigEditor extends KPart {
19
+ @property({ attribute: false })
20
+ public input?: EditorInput;
21
+
22
+ @state()
23
+ private aiConfig?: AIConfig;
24
+
25
+ @state()
26
+ private providers: ChatProvider[] = [];
27
+
28
+ @state()
29
+ private defaultProvider: string = '';
30
+
31
+ @state()
32
+ private editingCell: { rowIndex: number; field: string } | null = null;
33
+
34
+ @state()
35
+ private editingValue: string = '';
36
+
37
+ @state()
38
+ private hasChanges: boolean = false;
39
+
40
+ @state()
41
+ private availableModels: Array<{ id: string; name?: string }> = [];
42
+
43
+ @state()
44
+ private loadingModels: boolean = false;
45
+
46
+
47
+ @state()
48
+ private requireToolApproval: boolean = true;
49
+
50
+ @state()
51
+ private toolApprovalAllowlist: string[] = [];
52
+
53
+ @state()
54
+ private smartToolDetection: boolean = false;
55
+
56
+ @state()
57
+ private availableCommands: Array<{ id: string; name: string; description?: string }> = [];
58
+
59
+ private providerFactory: ProviderFactory = new ProviderFactory();
60
+
61
+ protected async doInitUI() {
62
+ await this.loadAvailableCommands();
63
+ await this.loadConfig();
64
+ subscribe(TOPIC_AICONFIG_CHANGED, () => {
65
+ this.loadConfig();
66
+ });
67
+ subscribe(TOPIC_SETTINGS_CHANGED, () => {
68
+ this.loadConfig();
69
+ });
70
+ }
71
+
72
+ private async loadConfig() {
73
+ const config = await appSettings.get(KEY_AI_CONFIG) as AIConfig | undefined;
74
+ this.aiConfig = config;
75
+
76
+ // Merge contributed providers with config providers
77
+ const contributedProviders = this.getContributedProviders();
78
+ const configProviders = config?.providers || [];
79
+ this.providers = this.mergeProviders(configProviders, contributedProviders);
80
+
81
+ const savedDefaultProvider = config?.defaultProvider || '';
82
+ // Validate that the default provider still exists in the providers list
83
+ if (savedDefaultProvider && !this.providers.find(p => p.name === savedDefaultProvider)) {
84
+ this.defaultProvider = '';
85
+ } else {
86
+ this.defaultProvider = savedDefaultProvider;
87
+ }
88
+ // Load requireToolApproval from config, default to true if missing
89
+ if (config?.requireToolApproval !== undefined) {
90
+ this.requireToolApproval = config.requireToolApproval;
91
+ } else {
92
+ this.requireToolApproval = true; // Default to true if missing
93
+ }
94
+ this.toolApprovalAllowlist = config?.toolApprovalAllowlist || [];
95
+ // Load smartToolDetection from config, default to false if missing
96
+ if (config?.smartToolDetection !== undefined) {
97
+ this.smartToolDetection = config.smartToolDetection;
98
+ } else {
99
+ this.smartToolDetection = false; // Default to false if missing
100
+ }
101
+ this.hasChanges = false;
102
+ this.markDirty(false);
103
+ this.editingCell = null;
104
+
105
+ // Ensure checkboxes are properly synced after render
106
+ await this.updateComplete;
107
+ this.syncCheckboxStates();
108
+ this.syncToolApprovalCheckbox();
109
+ this.syncSmartToolDetectionCheckbox();
110
+ }
111
+
112
+ private async loadAvailableCommands() {
113
+ const allCommands = commandRegistry.listCommands();
114
+ this.availableCommands = Object.entries(allCommands).map(([id, cmd]: [string, any]) => ({
115
+ id,
116
+ name: cmd.name || id,
117
+ description: cmd.description
118
+ })).sort((a, b) => a.name.localeCompare(b.name));
119
+ }
120
+
121
+ private getContributedProviders(): ChatProvider[] {
122
+ const contributions = contributionRegistry.getContributions(CID_CHAT_PROVIDERS) as ChatProviderContribution[];
123
+ return contributions.map(contrib => contrib.provider);
124
+ }
125
+
126
+ private mergeProviders(existing: ChatProvider[], contributed: ChatProvider[]): ChatProvider[] {
127
+ const existingNames = new Set(existing.map(p => p.name));
128
+ const missing = contributed.filter(provider => !existingNames.has(provider.name));
129
+ return missing.length > 0 ? [...existing, ...missing] : existing;
130
+ }
131
+
132
+
133
+ private syncCheckboxStates() {
134
+ const rows = this.shadowRoot?.querySelectorAll('tbody tr') as NodeListOf<HTMLElement>;
135
+ if (rows && this.providers) {
136
+ rows.forEach((row, index) => {
137
+ const checkbox = row.querySelector('td:first-child wa-checkbox') as any;
138
+ const provider = this.providers[index];
139
+ if (checkbox && provider) {
140
+ checkbox.checked = this.defaultProvider === provider.name;
141
+ }
142
+ });
143
+ }
144
+ }
145
+
146
+ private syncToolApprovalCheckbox() {
147
+ const checkbox = this.shadowRoot?.querySelector('.tool-approval-controls wa-checkbox') as any;
148
+ if (checkbox) {
149
+ checkbox.checked = this.requireToolApproval;
150
+ }
151
+ }
152
+
153
+ private syncSmartToolDetectionCheckbox() {
154
+ const checkbox = this.shadowRoot?.querySelector('.tool-detection-section wa-checkbox') as any;
155
+ if (checkbox) {
156
+ checkbox.checked = this.smartToolDetection;
157
+ }
158
+ }
159
+
160
+ private async saveConfig() {
161
+ if (!this.aiConfig) {
162
+ return;
163
+ }
164
+
165
+ const updatedConfig: AIConfig = {
166
+ ...this.aiConfig,
167
+ defaultProvider: this.defaultProvider,
168
+ providers: this.providers,
169
+ requireToolApproval: this.requireToolApproval,
170
+ toolApprovalAllowlist: this.toolApprovalAllowlist,
171
+ smartToolDetection: this.smartToolDetection
172
+ };
173
+
174
+ await appSettings.set(KEY_AI_CONFIG, updatedConfig);
175
+ this.hasChanges = false;
176
+ this.markDirty(false);
177
+ }
178
+
179
+ async save() {
180
+ if (!this.hasChanges) {
181
+ return;
182
+ }
183
+ try {
184
+ await this.saveConfig();
185
+ } catch (error) {
186
+ console.error('Failed to save AI config:', error);
187
+ throw error;
188
+ }
189
+ }
190
+
191
+ private async startCellEditing(rowIndex: number, field: string) {
192
+ const provider = this.providers[rowIndex];
193
+ if (!provider) return;
194
+
195
+ const value = this.getProviderFieldValue(provider, field);
196
+
197
+ this.editingCell = { rowIndex, field };
198
+ this.editingValue = value;
199
+
200
+ // Special handling for model field - fetch available models
201
+ // Do this after setting editing state so the spinner can render
202
+ if (field === 'model') {
203
+ await this.updateComplete; // Wait for editing state to render
204
+ await this.fetchModels(provider);
205
+ }
206
+ }
207
+
208
+ private async fetchModels(provider: ChatProvider): Promise<void> {
209
+ this.loadingModels = true;
210
+ this.availableModels = []; // Clear previous models
211
+ await this.updateComplete; // Wait for the update to complete so spinner can render
212
+
213
+ try {
214
+ // Get the provider instance - it handles model listing itself
215
+ const providerInstance = this.providerFactory.getProvider(provider);
216
+
217
+ if (providerInstance.getAvailableModels) {
218
+ const models = await providerInstance.getAvailableModels(provider);
219
+ this.availableModels = Array.isArray(models) ? models : [];
220
+ } else {
221
+ // Provider doesn't support model listing
222
+ this.availableModels = [];
223
+ }
224
+ } catch (error) {
225
+ this.availableModels = [];
226
+ } finally {
227
+ this.loadingModels = false;
228
+ }
229
+ }
230
+
231
+ private cancelCellEditing() {
232
+ this.editingCell = null;
233
+ this.editingValue = '';
234
+ this.availableModels = [];
235
+ this.loadingModels = false;
236
+ }
237
+
238
+ private saveCellEditing() {
239
+ if (!this.editingCell) return;
240
+
241
+ const { rowIndex, field } = this.editingCell;
242
+ this.updateProviderField(rowIndex, field as keyof ChatProvider, this.editingValue);
243
+ this.cancelCellEditing();
244
+ }
245
+
246
+ private markDirtyAndUpdate() {
247
+ this.hasChanges = true;
248
+ this.markDirty(true);
249
+ }
250
+
251
+ private getProviderFieldValue(provider: ChatProvider, field: string): string {
252
+ const fieldMap: Record<string, keyof ChatProvider> = {
253
+ 'name': 'name',
254
+ 'model': 'model',
255
+ 'chatApiEndpoint': 'chatApiEndpoint',
256
+ 'apiKey': 'apiKey',
257
+ 'ocrApiEndpoint': 'ocrApiEndpoint',
258
+ 'ocrModel': 'ocrModel'
259
+ };
260
+ const key = fieldMap[field];
261
+ if (!key) return '';
262
+ const value = provider[key];
263
+ return typeof value === 'string' ? value : '';
264
+ }
265
+
266
+ private updateProviderField(index: number, field: keyof ChatProvider, value: any) {
267
+ this.providers = this.providers.map((p, i) => {
268
+ if (i === index) {
269
+ const updated = { ...p };
270
+ if (field === 'ocrApiEndpoint' || field === 'ocrModel') {
271
+ updated[field] = value || undefined;
272
+ } else {
273
+ (updated as any)[field] = value;
274
+ }
275
+ return updated;
276
+ }
277
+ return p;
278
+ });
279
+ this.markDirtyAndUpdate();
280
+ }
281
+
282
+ private updateProviderParameter(index: number, paramKey: string, value: any) {
283
+ this.providers = this.providers.map((p, i) => {
284
+ if (i === index) {
285
+ const parameters = { ...(p.parameters || {}), [paramKey]: value };
286
+ return { ...p, parameters };
287
+ }
288
+ return p;
289
+ });
290
+ this.markDirtyAndUpdate();
291
+ }
292
+
293
+ private async deleteProvider(index: number) {
294
+ const providerToDelete = this.providers[index];
295
+ const confirmed = await confirmDialog(t('DELETE_PROVIDER_CONFIRM', { name: providerToDelete.name }));
296
+ if (confirmed) {
297
+ // Clear default provider if it's the one being deleted
298
+ if (this.defaultProvider === providerToDelete.name) {
299
+ this.defaultProvider = '';
300
+ }
301
+ this.providers = this.providers.filter((_, i) => i !== index);
302
+ this.markDirtyAndUpdate();
303
+ }
304
+ }
305
+
306
+ private addProvider() {
307
+ const newProvider: ChatProvider = {
308
+ name: 'new-provider',
309
+ model: '',
310
+ apiKey: '',
311
+ chatApiEndpoint: ''
312
+ };
313
+ this.providers = [...this.providers, newProvider];
314
+ this.markDirtyAndUpdate();
315
+ }
316
+
317
+ private setDefaultProvider(providerName: string) {
318
+ // Only set if it's different from current
319
+ if (this.defaultProvider !== providerName) {
320
+ this.defaultProvider = providerName;
321
+ this.markDirtyAndUpdate();
322
+ }
323
+ }
324
+
325
+ private createInputHandlers() {
326
+ return {
327
+ onInput: (e: Event) => {
328
+ const input = e.target as HTMLInputElement;
329
+ this.editingValue = input.value;
330
+ },
331
+ onKeydown: (e: KeyboardEvent) => {
332
+ if (e.key === 'Enter') {
333
+ e.preventDefault();
334
+ this.saveCellEditing();
335
+ } else if (e.key === 'Escape') {
336
+ e.preventDefault();
337
+ this.cancelCellEditing();
338
+ }
339
+ },
340
+ onBlur: () => {
341
+ this.saveCellEditing();
342
+ }
343
+ };
344
+ }
345
+
346
+ private renderEditableCell(
347
+ index: number,
348
+ field: string,
349
+ displayValue: string | TemplateResult,
350
+ inputType: 'text' | 'password' = 'text',
351
+ placeholder?: string,
352
+ customInput?: TemplateResult
353
+ ): TemplateResult {
354
+ const isEditing = this.editingCell?.rowIndex === index && this.editingCell?.field === field;
355
+ const handlers = this.createInputHandlers();
356
+
357
+ if (isEditing && customInput) {
358
+ return customInput;
359
+ }
360
+
361
+ if (isEditing) {
362
+ return html`
363
+ <wa-input
364
+ type="${inputType}"
365
+ .value="${this.editingValue}"
366
+ placeholder="${placeholder || ''}"
367
+ @input="${handlers.onInput}"
368
+ @keydown="${handlers.onKeydown}"
369
+ @blur="${handlers.onBlur}"
370
+ autofocus>
371
+ </wa-input>
372
+ `;
373
+ }
374
+
375
+ return html`<span>${displayValue}</span>`;
376
+ }
377
+
378
+ private renderModelCell(index: number, provider: ChatProvider): TemplateResult {
379
+ const isEditing = this.editingCell?.rowIndex === index && this.editingCell?.field === 'model';
380
+
381
+ if (!isEditing) {
382
+ return html`<span>${provider.model}</span>`;
383
+ }
384
+
385
+ return html`
386
+ ${when(this.loadingModels, () => html`
387
+ <wa-input
388
+ .value="${this.editingValue}"
389
+ placeholder="${t('LOADING_MODELS')}"
390
+ readonly>
391
+ <wa-animation name="spinner" play slot="start"></wa-animation>
392
+ </wa-input>
393
+ `, () => html`
394
+ ${when(this.availableModels.length > 0, () => html`
395
+ <wa-dropdown
396
+ @wa-select="${(e: CustomEvent) => {
397
+ const selectedValue = e.detail.item.value;
398
+ if (selectedValue) {
399
+ this.editingValue = selectedValue;
400
+ this.saveCellEditing();
401
+ }
402
+ }}"
403
+ placement="bottom-start">
404
+ <wa-input
405
+ slot="trigger"
406
+ .value="${this.editingValue}"
407
+ placeholder="${t('SELECT_MODEL')}"
408
+ readonly
409
+ @keydown="${(e: KeyboardEvent) => {
410
+ if (e.key === 'Escape') {
411
+ e.preventDefault();
412
+ this.cancelCellEditing();
413
+ }
414
+ }}">
415
+ </wa-input>
416
+ ${this.availableModels.map(model => html`
417
+ <wa-dropdown-item value="${model.id}">
418
+ ${model.name || model.id}
419
+ </wa-dropdown-item>
420
+ `)}
421
+ </wa-dropdown>
422
+ `, () => html`
423
+ ${this.renderEditableCell(index, 'model', provider.model)}
424
+ `)}
425
+ `)}
426
+ `;
427
+ }
428
+
429
+ render() {
430
+ return html`
431
+ <div class="ai-config-editor">
432
+ <div class="editor-header">
433
+ <h2>${t('PROVIDERS')}</h2>
434
+ <div class="header-actions">
435
+ <wa-button
436
+ variant="brand"
437
+ appearance="filled"
438
+ @click="${() => this.addProvider()}">
439
+ ${t('ADD_PROVIDER')}
440
+ </wa-button>
441
+ </div>
442
+ </div>
443
+
444
+ <div class="table-container">
445
+ <table class="providers-table">
446
+ <thead>
447
+ <tr>
448
+ <th>${t('DEFAULT')}</th>
449
+ <th>${t('NAME')}</th>
450
+ <th>${t('MODEL')}</th>
451
+ <th>${t('API_ENDPOINT')}</th>
452
+ <th>${t('API_KEY')}</th>
453
+ <th>${t('OCR_ENDPOINT')}</th>
454
+ <th>${t('OCR_MODEL')}</th>
455
+ <th>${t('ACTIONS')}</th>
456
+ </tr>
457
+ </thead>
458
+ <tbody>
459
+ ${repeat(this.providers, (provider, index) => index, (provider, index) => html`
460
+ <tr class="${this.editingCell?.rowIndex === index ? 'editing' : ''}">
461
+ <td>
462
+ <wa-checkbox
463
+ .checked="${this.defaultProvider === provider.name}"
464
+ @change="${async (e: Event) => {
465
+ const checkbox = e.target as any;
466
+ if (checkbox.checked) {
467
+ // When checking, uncheck all others and set this as default
468
+ this.setDefaultProvider(provider.name);
469
+ // Wait for update to complete, then ensure other default checkboxes are unchecked
470
+ await this.updateComplete;
471
+ // Select only checkboxes in the first column (Default column)
472
+ const rows = this.shadowRoot?.querySelectorAll('tbody tr') as NodeListOf<HTMLElement>;
473
+ if (rows) {
474
+ rows.forEach((row) => {
475
+ const cb = row.querySelector('td:first-child wa-checkbox') as any;
476
+ if (cb && cb !== checkbox) {
477
+ cb.checked = false;
478
+ }
479
+ });
480
+ }
481
+ } else {
482
+ // Prevent unchecking if this is already the default
483
+ if (this.defaultProvider === provider.name) {
484
+ // Re-check the checkbox to prevent unsetting the default
485
+ checkbox.checked = true;
486
+ }
487
+ }
488
+ }}">
489
+ </wa-checkbox>
490
+ </td>
491
+ <td class="editable-cell" @dblclick="${() => this.startCellEditing(index, 'name')}">
492
+ ${this.renderEditableCell(index, 'name', provider.name)}
493
+ </td>
494
+ <td class="editable-cell" @dblclick="${() => this.startCellEditing(index, 'model')}">
495
+ ${this.renderModelCell(index, provider)}
496
+ </td>
497
+ <td class="editable-cell" @dblclick="${() => this.startCellEditing(index, 'chatApiEndpoint')}">
498
+ ${this.renderEditableCell(index, 'chatApiEndpoint', html`<span class="endpoint-text">${provider.chatApiEndpoint}</span>`)}
499
+ </td>
500
+ <td class="editable-cell" @dblclick="${() => this.startCellEditing(index, 'apiKey')}">
501
+ ${this.renderEditableCell(index, 'apiKey', html`<span class="api-key-text">${provider.apiKey ? '••••••••' : ''}</span>`, 'password', t('API_KEY'))}
502
+ </td>
503
+ <td class="editable-cell" @dblclick="${() => this.startCellEditing(index, 'ocrApiEndpoint')}">
504
+ ${this.renderEditableCell(index, 'ocrApiEndpoint', provider.ocrApiEndpoint || '-', 'text', t('OPTIONAL'))}
505
+ </td>
506
+ <td class="editable-cell" @dblclick="${() => this.startCellEditing(index, 'ocrModel')}">
507
+ ${this.renderEditableCell(index, 'ocrModel', provider.ocrModel || '-', 'text', t('OPTIONAL'))}
508
+ </td>
509
+ <td>
510
+ <wa-button
511
+ variant="danger"
512
+ appearance="plain"
513
+ size="small"
514
+ @click="${() => this.deleteProvider(index)}">
515
+ ${t('DELETE_PROVIDER')}
516
+ </wa-button>
517
+ </td>
518
+ </tr>
519
+ `)}
520
+ </tbody>
521
+ </table>
522
+ </div>
523
+
524
+ ${when(this.providers.length === 0, () => html`
525
+ <div class="empty-state">
526
+ <p>${t('NO_PROVIDERS_CONFIGURED')}</p>
527
+ </div>
528
+ `)}
529
+
530
+ <div class="tool-approval-section">
531
+ <h3>${t('TOOL_APPROVALS')}</h3>
532
+ <div class="tool-approval-controls">
533
+ <wa-checkbox
534
+ .checked="${this.requireToolApproval}"
535
+ @change="${(e: Event) => {
536
+ const checkbox = e.target as any;
537
+ this.requireToolApproval = checkbox.checked;
538
+ this.markDirtyAndUpdate();
539
+ }}">
540
+ ${t('REQUIRE_APPROVAL_BEFORE_EXECUTING')}
541
+ </wa-checkbox>
542
+ </div>
543
+
544
+ <div class="tool-detection-section" style="margin-top: 1.5rem;">
545
+ <wa-checkbox
546
+ .checked="${this.smartToolDetection}"
547
+ @change="${(e: Event) => {
548
+ const checkbox = e.target as any;
549
+ this.smartToolDetection = checkbox.checked;
550
+ this.markDirtyAndUpdate();
551
+ }}">
552
+ ${t('SMART_TOOL_DETECTION')}
553
+ </wa-checkbox>
554
+ <p class="hint" style="margin-top: 0.5rem; margin-left: 1.75rem; color: var(--wa-color-text-secondary, #666); font-size: 0.875rem;">
555
+ ${t('SMART_TOOL_DETECTION_HINT')}
556
+ </p>
557
+ </div>
558
+
559
+ <div class="allowlist-section">
560
+ <h4>
561
+ ${t('APPROVED_COMMANDS')}
562
+ <span class="command-stats">
563
+ (${this.toolApprovalAllowlist.length}/${this.availableCommands.length})
564
+ </span>
565
+ </h4>
566
+ <p class="hint">
567
+ ${this.requireToolApproval
568
+ ? t('SELECT_COMMANDS_WITHOUT_APPROVAL')
569
+ : t('COMMANDS_AUTO_APPROVED')}
570
+ </p>
571
+ <div class="commands-list ${!this.requireToolApproval ? 'disabled' : ''}">
572
+ ${this.availableCommands.map(cmd => html`
573
+ <div class="command-item">
574
+ <wa-checkbox
575
+ .checked="${this.toolApprovalAllowlist.includes(cmd.id)}"
576
+ ?disabled="${!this.requireToolApproval}"
577
+ @change="${(e: Event) => {
578
+ const checkbox = e.target as any;
579
+ if (checkbox.checked) {
580
+ if (!this.toolApprovalAllowlist.includes(cmd.id)) {
581
+ this.toolApprovalAllowlist = [...this.toolApprovalAllowlist, cmd.id];
582
+ this.markDirtyAndUpdate();
583
+ }
584
+ } else {
585
+ this.toolApprovalAllowlist = this.toolApprovalAllowlist.filter(id => id !== cmd.id);
586
+ this.markDirtyAndUpdate();
587
+ }
588
+ }}">
589
+ <div class="command-label">
590
+ ${cmd.name}
591
+ ${cmd.description ? html`
592
+ <span class="command-description">${cmd.description}</span>
593
+ ` : ''}
594
+ </div>
595
+ </wa-checkbox>
596
+ </div>
597
+ `)}
598
+ </div>
599
+ </div>
600
+ </div>
601
+ </div>
602
+ `;
603
+ }
604
+
605
+ static styles = css`
606
+ :host {
607
+ display: block;
608
+ height: 100%;
609
+ overflow: auto;
610
+ }
611
+
612
+ .ai-config-editor {
613
+ display: flex;
614
+ flex-direction: column;
615
+ height: 100%;
616
+ padding: 1rem;
617
+ }
618
+
619
+ .editor-header {
620
+ display: flex;
621
+ justify-content: space-between;
622
+ align-items: center;
623
+ margin-bottom: 1rem;
624
+ padding-bottom: 1rem;
625
+ }
626
+
627
+ .editor-header h2 {
628
+ margin: 0;
629
+ font-size: 1.25rem;
630
+ font-weight: 600;
631
+ color: var(--wa-color-text-normal);
632
+ }
633
+
634
+ .header-actions {
635
+ display: flex;
636
+ gap: 0.5rem;
637
+ }
638
+
639
+ .table-container {
640
+ flex: 1;
641
+ overflow: auto;
642
+ border: solid var(--wa-border-width-s) var(--wa-color-neutral-border-loud);
643
+ border-radius: var(--wa-border-radius-m);
644
+ }
645
+
646
+ .providers-table {
647
+ width: 100%;
648
+ border-collapse: collapse;
649
+ background-color: var(--wa-color-surface-raised);
650
+ }
651
+
652
+ .providers-table thead {
653
+ position: sticky;
654
+ top: 0;
655
+ background-color: var(--wa-color-surface-raised);
656
+ z-index: 1;
657
+ }
658
+
659
+ .providers-table th {
660
+ padding: 0.75rem;
661
+ text-align: left;
662
+ font-weight: 600;
663
+ font-size: 0.875rem;
664
+ color: var(--wa-color-text-subtle);
665
+ border-bottom: solid var(--wa-border-width-s) var(--wa-color-neutral-border-loud);
666
+ white-space: nowrap;
667
+ }
668
+
669
+ .providers-table td {
670
+ padding: 0.75rem;
671
+ border-bottom: solid var(--wa-border-width-s) var(--wa-color-neutral-border-subtle);
672
+ vertical-align: middle;
673
+ }
674
+
675
+ .providers-table tbody tr:hover {
676
+ background-color: var(--wa-color-surface-lowered);
677
+ }
678
+
679
+ .providers-table tbody tr.editing {
680
+ background-color: var(--wa-color-surface-brand-subtle);
681
+ }
682
+
683
+ .providers-table tbody tr:last-child td {
684
+ border-bottom: none;
685
+ }
686
+
687
+ .providers-table wa-input {
688
+ width: 100%;
689
+ min-width: 150px;
690
+ }
691
+
692
+ .providers-table wa-dropdown {
693
+ width: 100%;
694
+ min-width: 150px;
695
+ }
696
+
697
+ .providers-table wa-dropdown wa-input {
698
+ width: 100%;
699
+ }
700
+
701
+ .providers-table wa-checkbox {
702
+ display: flex;
703
+ justify-content: center;
704
+ }
705
+
706
+ .editable-cell {
707
+ cursor: pointer;
708
+ position: relative;
709
+ }
710
+
711
+ .editable-cell:hover {
712
+ background-color: var(--wa-color-surface-lowered);
713
+ }
714
+
715
+ .editable-cell span {
716
+ display: block;
717
+ min-height: 1.5rem;
718
+ padding: 0.25rem 0;
719
+ }
720
+
721
+ .endpoint-text {
722
+ font-family: var(--wa-font-mono);
723
+ font-size: 0.875rem;
724
+ color: var(--wa-color-text-subtle);
725
+ word-break: break-all;
726
+ max-width: 200px;
727
+ display: inline-block;
728
+ }
729
+
730
+ .api-key-text {
731
+ font-family: var(--wa-font-mono);
732
+ font-size: 0.875rem;
733
+ color: var(--wa-color-text-subtle);
734
+ }
735
+
736
+ .empty-state {
737
+ display: flex;
738
+ justify-content: center;
739
+ align-items: center;
740
+ padding: 3rem;
741
+ color: var(--wa-color-text-subtle);
742
+ }
743
+
744
+ .empty-state p {
745
+ margin: 0;
746
+ }
747
+
748
+ .tool-approval-section {
749
+ margin-top: 2rem;
750
+ padding-top: 2rem;
751
+ }
752
+
753
+ .tool-approval-section h3 {
754
+ margin: 0 0 1rem 0;
755
+ font-size: 1.125rem;
756
+ font-weight: 600;
757
+ color: var(--wa-color-text-normal);
758
+ }
759
+
760
+ .tool-approval-controls {
761
+ margin-bottom: 1rem;
762
+ }
763
+
764
+ .allowlist-section {
765
+ margin-top: 1.5rem;
766
+ }
767
+
768
+ .allowlist-section h4 {
769
+ margin: 0 0 0.5rem 0;
770
+ font-size: 1rem;
771
+ font-weight: 500;
772
+ color: var(--wa-color-text-normal);
773
+ display: flex;
774
+ align-items: center;
775
+ gap: 0.5rem;
776
+ }
777
+
778
+ .command-stats {
779
+ font-size: 0.875rem;
780
+ font-weight: normal;
781
+ color: var(--wa-color-text-subtle);
782
+ }
783
+
784
+ .allowlist-section .hint {
785
+ margin: 0 0 1rem 0;
786
+ font-size: 0.875rem;
787
+ color: var(--wa-color-text-subtle);
788
+ }
789
+
790
+ .commands-list {
791
+ display: flex;
792
+ flex-direction: column;
793
+ gap: 0.5rem;
794
+ max-height: 300px;
795
+ overflow-y: auto;
796
+ padding: 0.5rem;
797
+ border: solid var(--wa-border-width-s) var(--wa-color-neutral-border-subtle);
798
+ border-radius: var(--wa-border-radius-m);
799
+ background-color: var(--wa-color-surface-raised);
800
+ }
801
+
802
+ .commands-list.disabled {
803
+ opacity: 0.6;
804
+ }
805
+
806
+ .command-item {
807
+ padding: 0.25rem 0;
808
+ }
809
+
810
+ .command-item wa-checkbox {
811
+ width: 100%;
812
+ }
813
+
814
+ .command-label {
815
+ display: flex;
816
+ flex-direction: column;
817
+ gap: 0.125rem;
818
+ }
819
+
820
+ .command-description {
821
+ font-size: 0.75rem;
822
+ color: var(--wa-color-text-subtle);
823
+ font-weight: normal;
824
+ }
825
+
826
+ `;
827
+ }
828
+