@eclipse-lyra/extension-ai-system 0.7.57 → 0.7.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2326 @@
1
+ import { A as CID_AGENTS, B as TOPIC_AICONFIG_CHANGED, H as TOPIC_AI_STREAM_COMPLETE, M as CID_PROMPT_ENHANCERS, P as KEY_AI_CONFIG, b as ProviderFactory, j as CID_CHAT_PROVIDERS, n as EMPTY_USAGE, r as tokenUsageTracker, t as aiService } from "./ai-service-CQaQJORq.js";
2
+ import { LyraElement, LyraPart, SIDEBAR_AUXILIARY, TOOLBAR_BOTTOM, TOOLBAR_MAIN_RIGHT, TOPIC_SETTINGS_CHANGED, appSettings, commandRegistry, confirmDialog, contributionRegistry, editorRegistry, registerAll, rootContext, subscribe, taskService, toastError, uiContext, workspaceService } from "@eclipse-lyra/core";
3
+ import { LitElement, css, html, nothing } from "lit";
4
+ import { customElement, property, query, state } from "lit/decorators.js";
5
+ import { when } from "lit/directives/when.js";
6
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
7
+ import { marked } from "marked";
8
+ import _decorate from "@oxc-project/runtime/helpers/decorate";
9
+ import { repeat } from "lit/directives/repeat.js";
10
+ //#region src/general-assistant-prompt.txt?raw
11
+ var general_assistant_prompt_default = "You are an assistant in a web application with workspace, editors, and AI chat.\n\n**Tools:**\nCommands are exposed as AI-callable tools. Tools are context-aware - available commands depend on active editor, selected files, and workspace state.\n\n**Tool Usage Rules:**\n1. If tools are available and match the request, use them - don't describe manual steps\n2. Read tool descriptions/parameters to select the correct tool\n3. Call tools in sequence for multi-step tasks\n4. After successful tool execution, provide a final response - don't loop or call more tools unless explicitly requested\n5. If no tools are available, explain what context is needed\n\nKeep responses concise. Use tools when available rather than discussing alternatives.\n\n";
12
+ //#endregion
13
+ //#region src/chat-provider-contributions.ts
14
+ for (const { label, ...provider } of [
15
+ {
16
+ label: "Ollama (Local)",
17
+ name: "ollama",
18
+ model: "gemma3:12b",
19
+ chatApiEndpoint: "https://<your-server>/v1/chat/completions",
20
+ apiKey: ""
21
+ },
22
+ {
23
+ label: "OpenWebUI (Self Hosted)",
24
+ name: "openwebui",
25
+ model: "gemma3:12b",
26
+ chatApiEndpoint: "https://<your-server>/api/v1/chat/completion",
27
+ apiKey: ""
28
+ },
29
+ {
30
+ label: "OpenAI",
31
+ name: "openai",
32
+ model: "gpt-4.1",
33
+ chatApiEndpoint: "https://api.openai.com/v1/chat/completions",
34
+ apiKey: "<your api key>"
35
+ },
36
+ {
37
+ label: "Groq",
38
+ name: "groq",
39
+ model: "llama-3.1-8b-instant",
40
+ chatApiEndpoint: "https://api.groq.com/openai/v1/chat/completions",
41
+ apiKey: "<your api key>"
42
+ },
43
+ {
44
+ label: "Cerebras",
45
+ name: "cerebras",
46
+ model: "llama3.1-8b",
47
+ chatApiEndpoint: "https://api.cerebras.ai/v1/chat/completions",
48
+ apiKey: "<your api key>"
49
+ },
50
+ {
51
+ label: "WebLLM",
52
+ name: "webllm",
53
+ model: "gemma-2-9b-it-q4f16_1-MLC",
54
+ chatApiEndpoint: "",
55
+ apiKey: "",
56
+ parameters: { context_window_size: 4096 }
57
+ },
58
+ {
59
+ label: "Mistral",
60
+ name: "mistral",
61
+ model: "mistral-large-latest",
62
+ chatApiEndpoint: "https://api.mistral.ai/v1/chat/completions",
63
+ apiKey: "<your api key>"
64
+ },
65
+ {
66
+ label: "LiteLLM",
67
+ name: "litellm",
68
+ model: "gpt-3.5-turbo",
69
+ chatApiEndpoint: "https://<your-server>/v1/chat/completions",
70
+ apiKey: "<your api key>"
71
+ }
72
+ ]) contributionRegistry.registerContribution(CID_CHAT_PROVIDERS, {
73
+ target: CID_CHAT_PROVIDERS,
74
+ label,
75
+ provider
76
+ });
77
+ //#endregion
78
+ //#region src/prompt-enhancer-contributions.ts
79
+ contributionRegistry.registerContribution(CID_PROMPT_ENHANCERS, {
80
+ label: "App State Enhancer",
81
+ enhancer: {
82
+ priority: 20,
83
+ enhance: async (prompt, _context) => {
84
+ try {
85
+ const workspace = await workspaceService.getWorkspace();
86
+ const activeEditor = editorRegistry.getEditorArea()?.getActiveEditor();
87
+ const appState = {
88
+ workspace: workspace?.getName() || null,
89
+ activeEditor: activeEditor ? {
90
+ title: activeEditor.input?.title || null,
91
+ editorId: activeEditor.input?.editorId || null
92
+ } : null
93
+ };
94
+ return `${prompt}\n\n***App's state:***\n${JSON.stringify(appState, null, 2)}`;
95
+ } catch {
96
+ return prompt;
97
+ }
98
+ }
99
+ }
100
+ });
101
+ //#endregion
102
+ //#region src/view/session-manager.ts
103
+ var SessionManager = class {
104
+ constructor() {
105
+ this.activeSession = null;
106
+ this.pastSessions = [];
107
+ }
108
+ async load() {
109
+ const saved = await appSettings.get("aiChatSessions");
110
+ if (!saved) return;
111
+ if (saved.active && Array.isArray(saved.history)) this.activeSession = saved.active;
112
+ else if (saved.activeSessionId && Array.isArray(saved.sessions)) {
113
+ this.activeSession = saved.sessions.find((s) => s.id === saved.activeSessionId) || null;
114
+ this.pastSessions = saved.sessions.filter((s) => s.id !== saved.activeSessionId);
115
+ } else if (Array.isArray(saved.all)) {
116
+ const [first, ...rest] = saved.all.sort((a, b) => b.updatedAt - a.updatedAt);
117
+ this.activeSession = first || null;
118
+ this.pastSessions = rest;
119
+ }
120
+ }
121
+ async persist() {
122
+ const all = [];
123
+ if (this.activeSession) all.push(this.activeSession);
124
+ all.push(...this.pastSessions);
125
+ await appSettings.set("aiChatSessions", {
126
+ all,
127
+ activeSessionId: this.activeSession?.id || null
128
+ });
129
+ }
130
+ createSession() {
131
+ const session = {
132
+ id: `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
133
+ history: [],
134
+ title: "New Chat",
135
+ createdAt: Date.now(),
136
+ updatedAt: Date.now()
137
+ };
138
+ if (this.activeSession) this.pastSessions.unshift(this.activeSession);
139
+ this.activeSession = session;
140
+ this.persist();
141
+ return session;
142
+ }
143
+ getActiveSession() {
144
+ return this.activeSession;
145
+ }
146
+ getActiveSessionId() {
147
+ return this.activeSession?.id || "";
148
+ }
149
+ switchToSession(sessionId) {
150
+ if (this.activeSession?.id === sessionId) return true;
151
+ const idx = this.pastSessions.findIndex((s) => s.id === sessionId);
152
+ if (idx === -1) return false;
153
+ const [target] = this.pastSessions.splice(idx, 1);
154
+ if (!target) return false;
155
+ if (this.activeSession) this.pastSessions.unshift(this.activeSession);
156
+ this.activeSession = target;
157
+ this.persist();
158
+ return true;
159
+ }
160
+ getPastSessions() {
161
+ return this.pastSessions;
162
+ }
163
+ deletePastSession(sessionId) {
164
+ const idx = this.pastSessions.findIndex((s) => s.id === sessionId);
165
+ if (idx === -1) return false;
166
+ this.pastSessions.splice(idx, 1);
167
+ this.persist();
168
+ return true;
169
+ }
170
+ addMessage(message) {
171
+ if (!this.activeSession) return;
172
+ this.activeSession.history.push(message);
173
+ this.activeSession.updatedAt = Date.now();
174
+ this.persist();
175
+ }
176
+ setTitle(title) {
177
+ if (!this.activeSession) return;
178
+ this.activeSession.title = title;
179
+ this.persist();
180
+ }
181
+ generateTitle(prompt) {
182
+ const trimmed = prompt.trim();
183
+ if (!trimmed) return "New Chat";
184
+ return trimmed.length <= 30 ? trimmed : trimmed.substring(0, 30).trim() + "...";
185
+ }
186
+ deleteActiveAndSwitchToFirst() {
187
+ if (!this.activeSession) return;
188
+ this.activeSession = this.pastSessions.shift() || null;
189
+ if (!this.activeSession) this.createSession();
190
+ this.persist();
191
+ }
192
+ };
193
+ //#endregion
194
+ //#region src/view/stream-manager.ts
195
+ var StreamManager = class {
196
+ constructor(onUpdate) {
197
+ this.streamingMessages = /* @__PURE__ */ new Map();
198
+ this.currentIndex = -1;
199
+ this.pendingUpdate = false;
200
+ this.onUpdate = onUpdate;
201
+ }
202
+ createStreamingMessage(role) {
203
+ const index = ++this.currentIndex;
204
+ this.streamingMessages.set(index, {
205
+ message: {
206
+ role,
207
+ content: ""
208
+ },
209
+ isStreaming: true
210
+ });
211
+ return index;
212
+ }
213
+ updateStreamingMessage(index, token) {
214
+ const msg = this.streamingMessages.get(index);
215
+ if (!msg) return;
216
+ msg.message.content += token;
217
+ this.scheduleUpdate();
218
+ }
219
+ completeStreamingMessage(index, message) {
220
+ const msg = this.streamingMessages.get(index);
221
+ if (!msg) return;
222
+ msg.message = message;
223
+ msg.isStreaming = false;
224
+ }
225
+ removeStreamingMessage(index) {
226
+ this.streamingMessages.delete(index);
227
+ }
228
+ findStreamingMessage(role) {
229
+ return Array.from(this.streamingMessages.values()).find((m) => m.message.role === role)?.message;
230
+ }
231
+ getAllStreamingMessages() {
232
+ return Array.from(this.streamingMessages.values());
233
+ }
234
+ scheduleUpdate() {
235
+ if (this.pendingUpdate) return;
236
+ this.pendingUpdate = true;
237
+ this.rafHandle = requestAnimationFrame(() => {
238
+ this.pendingUpdate = false;
239
+ this.onUpdate?.();
240
+ });
241
+ }
242
+ cancelUpdates() {
243
+ if (this.rafHandle !== void 0) {
244
+ cancelAnimationFrame(this.rafHandle);
245
+ this.rafHandle = void 0;
246
+ this.pendingUpdate = false;
247
+ }
248
+ }
249
+ reset() {
250
+ this.streamingMessages.clear();
251
+ this.cancelUpdates();
252
+ this.currentIndex = -1;
253
+ }
254
+ };
255
+ //#endregion
256
+ //#region src/view/provider-manager.ts
257
+ var VIEW_SETTINGS_KEY = "aiViewChat";
258
+ var ProviderManager = class {
259
+ constructor(aiService) {
260
+ this.aiService = aiService;
261
+ this.providers = [];
262
+ this.availableModels = [];
263
+ this.loadingModels = false;
264
+ this.providerFactory = new ProviderFactory();
265
+ }
266
+ async initialize() {
267
+ this.providers = await this.aiService.getProviders() || [];
268
+ const defaultProvider = await this.aiService.getDefaultProvider();
269
+ if (defaultProvider) this.selectedProvider = defaultProvider;
270
+ }
271
+ getProviders() {
272
+ return this.providers;
273
+ }
274
+ getSelectedProvider() {
275
+ return this.selectedProvider;
276
+ }
277
+ setSelectedProvider(provider) {
278
+ this.selectedProvider = provider;
279
+ }
280
+ getAvailableModels() {
281
+ return this.availableModels;
282
+ }
283
+ isLoadingModels() {
284
+ return this.loadingModels;
285
+ }
286
+ async saveSettings(providerName, model, apiKey, requireToolApproval, toolApprovalAllowlist) {
287
+ const settings = { ...await appSettings.get(VIEW_SETTINGS_KEY) || {} };
288
+ if (requireToolApproval !== void 0) settings.requireToolApproval = requireToolApproval;
289
+ if (toolApprovalAllowlist !== void 0) settings.toolApprovalAllowlist = toolApprovalAllowlist;
290
+ await appSettings.set(VIEW_SETTINGS_KEY, settings);
291
+ const provider = this.providers.find((p) => p.name === providerName);
292
+ if (provider) {
293
+ this.selectedProvider = {
294
+ ...provider,
295
+ model,
296
+ ...apiKey !== void 0 && { apiKey }
297
+ };
298
+ await this.updateProviderInAIConfig(providerName, {
299
+ model,
300
+ ...apiKey !== void 0 && { apiKey }
301
+ });
302
+ await this.aiService.setDefaultProvider(providerName);
303
+ }
304
+ }
305
+ async updateProviderInAIConfig(providerName, updates) {
306
+ const aiConfig = await appSettings.get("aiConfig") || {};
307
+ if (!aiConfig.providers || !Array.isArray(aiConfig.providers)) return;
308
+ const idx = aiConfig.providers.findIndex((p) => p.name === providerName);
309
+ if (idx >= 0) {
310
+ aiConfig.providers[idx] = {
311
+ ...aiConfig.providers[idx],
312
+ ...updates
313
+ };
314
+ await appSettings.set(KEY_AI_CONFIG, aiConfig);
315
+ }
316
+ }
317
+ async loadToolApprovalAllowlist() {
318
+ return (await appSettings.get(VIEW_SETTINGS_KEY) || {}).toolApprovalAllowlist || [];
319
+ }
320
+ async fetchModels(providerName) {
321
+ const provider = this.providers.find((p) => p.name === providerName);
322
+ if (!provider) return;
323
+ this.loadingModels = true;
324
+ this.availableModels = [];
325
+ try {
326
+ this.availableModels = await this.providerFactory.getProvider(provider).getAvailableModels?.(provider) ?? [];
327
+ } finally {
328
+ this.loadingModels = false;
329
+ }
330
+ }
331
+ };
332
+ //#endregion
333
+ //#region src/view/agent-group-manager.ts
334
+ var AgentGroupManager = class {
335
+ constructor() {
336
+ this.groups = /* @__PURE__ */ new Map();
337
+ }
338
+ createGroup(sessionId, userMessageIndex, userMessage, roles, getAgentMetadata) {
339
+ const groupId = `group-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
340
+ this.currentGroupId = groupId;
341
+ const group = {
342
+ id: groupId,
343
+ sessionId,
344
+ userMessageIndex,
345
+ userMessage,
346
+ timestamp: /* @__PURE__ */ new Date(),
347
+ agents: /* @__PURE__ */ new Map(),
348
+ messageIndices: /* @__PURE__ */ new Map()
349
+ };
350
+ roles.forEach((role) => {
351
+ const { label, icon } = getAgentMetadata(role);
352
+ group.agents.set(role, {
353
+ role,
354
+ label,
355
+ icon,
356
+ status: "streaming"
357
+ });
358
+ });
359
+ this.groups.set(groupId, group);
360
+ return groupId;
361
+ }
362
+ getGroup(groupId) {
363
+ return this.groups.get(groupId);
364
+ }
365
+ updateAgentStatus(groupId, role, status, message, messageIndex) {
366
+ const group = this.groups.get(groupId);
367
+ if (!group) return;
368
+ const agentInfo = group.agents.get(role);
369
+ if (!agentInfo) return;
370
+ agentInfo.status = status;
371
+ if (message) agentInfo.message = message;
372
+ if (messageIndex !== void 0) {
373
+ agentInfo.messageIndex = messageIndex;
374
+ group.messageIndices.set(role, messageIndex);
375
+ }
376
+ }
377
+ getGroupsForSession(sessionId) {
378
+ return Array.from(this.groups.values()).filter((g) => g.sessionId === sessionId);
379
+ }
380
+ findGroupForUserMessage(sessionId, userMessageIndex, userMessage) {
381
+ return Array.from(this.groups.values()).find((g) => g.sessionId === sessionId && g.userMessageIndex === userMessageIndex && g.userMessage === userMessage);
382
+ }
383
+ findGroupForMessage(sessionId, messageRole, messageIndex) {
384
+ return Array.from(this.groups.values()).find((g) => g.sessionId === sessionId && g.messageIndices.get(messageRole) === messageIndex);
385
+ }
386
+ getCurrentGroupId() {
387
+ return this.currentGroupId;
388
+ }
389
+ setCurrentGroupId(groupId) {
390
+ this.currentGroupId = groupId;
391
+ }
392
+ clearCurrentGroup() {
393
+ this.currentGroupId = void 0;
394
+ }
395
+ getAllGroups() {
396
+ return Array.from(this.groups.values());
397
+ }
398
+ clearAll() {
399
+ this.groups.clear();
400
+ this.currentGroupId = void 0;
401
+ }
402
+ };
403
+ //#endregion
404
+ //#region src/view/components/ai-chat-message.ts
405
+ var AIChatMessage = class AIChatMessage extends LitElement {
406
+ constructor(..._args) {
407
+ super(..._args);
408
+ this.isStreaming = false;
409
+ this.showHeader = true;
410
+ }
411
+ updated(_changedProperties) {
412
+ super.updated(_changedProperties);
413
+ if (_changedProperties.has("message") || !this.hasAttribute("data-is-user")) this.updateAlignment();
414
+ }
415
+ updateAlignment() {
416
+ if (this.message) this.setAttribute("data-is-user", String(this.message.role === "user"));
417
+ }
418
+ copyToClipboard(text) {
419
+ navigator.clipboard.writeText(text).catch((err) => console.error("Failed to copy:", err));
420
+ }
421
+ processMarkdownContent(markdownHtml) {
422
+ if (markdownHtml.includes("code-blocwrapper")) return markdownHtml;
423
+ return markdownHtml.replace(/<pre><code([^>]*)>([\s\S]*?)<\/code><\/pre>/gi, (_, attrs, codeText) => `
424
+ <div class="code-blocwrapper">
425
+ <div class="code-blocheader">
426
+ <wa-copy-button value="${this.escapeHtmlAttribute(codeText.trim())}" size="small" label="Copy code"></wa-copy-button>
427
+ </div>
428
+ <div class="code-bloccontent">
429
+ <pre><code${attrs}>${codeText}</code></pre>
430
+ </div>
431
+ </div>`);
432
+ }
433
+ escapeHtmlAttribute(text) {
434
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
435
+ }
436
+ handleResend(e) {
437
+ e?.preventDefault();
438
+ e?.stopPropagation();
439
+ if (!this.message) return;
440
+ this.dispatchEvent(new CustomEvent("resend", {
441
+ detail: {
442
+ message: this.message,
443
+ messageIndex: this.messageIndex
444
+ },
445
+ bubbles: true,
446
+ composed: true
447
+ }));
448
+ }
449
+ render() {
450
+ if (!this.message) return html``;
451
+ const message = this.message;
452
+ const isUser = message.role === "user";
453
+ return html`
454
+ <div class="message-wrapper ${isUser ? "user" : "assistant"} ${this.isStreaming ? "streaming" : ""}">
455
+ ${when(this.showHeader && !isUser, () => html`
456
+ <div class="message-header">
457
+ <div class="message-meta">
458
+ <wa-icon name="robot" label="${message.role}"></wa-icon>
459
+ <span class="role-name">${message.role}</span>
460
+ </div>
461
+ <div class="message-actions">
462
+ <wa-button variant="neutral" appearance="plain" size="small" title="Copy"
463
+ @click="${() => this.copyToClipboard(message.content)}">
464
+ <wa-icon slot="label" name="copy" label="Copy"></wa-icon>
465
+ </wa-button>
466
+ </div>
467
+ </div>
468
+ `)}
469
+ <div class="message-content-wrapper ${isUser ? "user" : ""}">
470
+ <div class="message-content">
471
+ ${unsafeHTML(this.processMarkdownContent(marked.parse(message.content || "")))}
472
+ ${when(this.isStreaming, () => html`<span class="streaming-cursor">▋</span>`)}
473
+ </div>
474
+ ${when(isUser, () => html`
475
+ <wa-button variant="neutral" appearance="plain" size="small" title="Copy"
476
+ @click="${() => this.copyToClipboard(message.content)}">
477
+ <wa-icon name="copy" label="Copy"></wa-icon>
478
+ </wa-button>
479
+ <wa-button variant="neutral" appearance="plain" size="small" title="Resend"
480
+ @click="${(e) => this.handleResend(e)}">
481
+ <wa-icon name="rotate-right" label="Resend"></wa-icon>
482
+ </wa-button>
483
+ `)}
484
+ </div>
485
+ </div>
486
+ `;
487
+ }
488
+ static {
489
+ this.styles = css`
490
+ :host {
491
+ display: flex;
492
+ flex-direction: column;
493
+ width: 100%;
494
+ max-width: 85%;
495
+ box-sizing: border-box;
496
+ animation: slideIn 0.2s ease-out;
497
+ }
498
+
499
+ :host([data-is-user="true"]) { align-self: flex-end; }
500
+ :host([data-is-user="false"]) { align-self: flex-start; }
501
+
502
+ @keyframes slideIn {
503
+ from { opacity: 0; transform: translateY(10px); }
504
+ to { opacity: 1; transform: translateY(0); }
505
+ }
506
+
507
+ .message-wrapper {
508
+ display: flex;
509
+ flex-direction: column;
510
+ gap: 0.5rem;
511
+ width: 100%;
512
+ box-sizing: border-box;
513
+ }
514
+
515
+ .message-header {
516
+ display: flex;
517
+ justify-content: space-between;
518
+ align-items: center;
519
+ gap: 0.5rem;
520
+ padding: 0 0.5rem;
521
+ }
522
+
523
+ .message-meta {
524
+ display: flex;
525
+ align-items: center;
526
+ gap: 0.5rem;
527
+ font-size: 0.875rem;
528
+ color: var(--wa-color-text-quiet);
529
+ }
530
+
531
+ .role-name { text-transform: capitalize; }
532
+
533
+ .message-actions {
534
+ display: flex;
535
+ gap: 0.25rem;
536
+ opacity: 0;
537
+ transition: opacity 0.2s;
538
+ }
539
+
540
+ .message-wrapper:hover .message-actions,
541
+ :host:hover .message-actions { opacity: 1; }
542
+
543
+ .message-content-wrapper {
544
+ display: flex;
545
+ align-items: flex-start;
546
+ gap: 0.5rem;
547
+ width: 100%;
548
+ }
549
+
550
+ .message-content-wrapper.user {
551
+ flex-direction: row;
552
+ align-items: center;
553
+ }
554
+
555
+ .message-content {
556
+ padding: 0.5rem 0.75rem;
557
+ border-radius: 0.25rem;
558
+ background-color: var(--wa-color-surface-default);
559
+ word-break: breaword;
560
+ overflow-wrap: breaword;
561
+ max-width: 100%;
562
+ box-sizing: border-box;
563
+ line-height: 1.3;
564
+ font-size: 0.9rem;
565
+ }
566
+
567
+ .message-content-wrapper.user .message-content {
568
+ padding: 0.0625rem 0.75rem;
569
+ background-color: var(--wa-color-brand-fill-quiet);
570
+ color: var(--wa-color-text-normal);
571
+ line-height: 1.4;
572
+ flex: 1;
573
+ }
574
+
575
+ .message-content p { margin: 0; padding: 0; }
576
+ .message-content ul, .message-content ol { margin: 0.25rem 0; padding-left: 1.25rem; }
577
+ .message-content li { margin: 0.125rem 0; padding: 0; line-height: 1.3; }
578
+ .message-content :first-child { margin-top: 0; padding-top: 0; }
579
+ .message-content :last-child { margin-bottom: 0; padding-bottom: 0; }
580
+
581
+ .message-content pre {
582
+ white-space: pre-wrap;
583
+ word-break: breaall;
584
+ max-width: 100%;
585
+ box-sizing: border-box;
586
+ overflow-x: auto;
587
+ margin: 0;
588
+ }
589
+
590
+ .message-content code {
591
+ font-family: 'Courier New', monospace;
592
+ background-color: var(--wa-color-surface-lowered);
593
+ padding: 0.125rem 0.25rem;
594
+ border-radius: 0.125rem;
595
+ }
596
+
597
+ .message-content pre code { background-color: transparent; padding: 0; display: block; }
598
+
599
+ .code-blocwrapper {
600
+ margin: 0.75rem 0;
601
+ border: solid var(--wa-border-width-s) var(--wa-color-neutral-border-loud);
602
+ border-radius: var(--wa-border-radius-m);
603
+ background-color: var(--wa-color-surface-lowered);
604
+ overflow: hidden;
605
+ }
606
+
607
+ .code-blocheader {
608
+ display: flex;
609
+ justify-content: flex-end;
610
+ align-items: center;
611
+ padding: 0.375rem 0.5rem;
612
+ border-bottom: solid var(--wa-border-width-s) var(--wa-color-neutral-border-loud);
613
+ background-color: var(--wa-color-surface-default);
614
+ }
615
+
616
+ .code-bloccontent { padding: 0.75rem; overflow-x: auto; }
617
+ .code-bloccontent pre { margin: 0; background-color: transparent; }
618
+ .code-bloccontent code { background-color: transparent; padding: 0; }
619
+
620
+ .streaming-cursor {
621
+ display: inline-block;
622
+ animation: blink 1s infinite;
623
+ color: var(--wa-color-brand-50);
624
+ }
625
+
626
+ @keyframes blink {
627
+ 0%, 50% { opacity: 1; }
628
+ 51%, 100% { opacity: 0; }
629
+ }
630
+ `;
631
+ }
632
+ };
633
+ _decorate([property({
634
+ type: Object,
635
+ attribute: false
636
+ })], AIChatMessage.prototype, "message", void 0);
637
+ _decorate([property({ type: Boolean })], AIChatMessage.prototype, "isStreaming", void 0);
638
+ _decorate([property({ type: Boolean })], AIChatMessage.prototype, "showHeader", void 0);
639
+ _decorate([property({
640
+ type: Number,
641
+ attribute: false
642
+ })], AIChatMessage.prototype, "messageIndex", void 0);
643
+ AIChatMessage = _decorate([customElement("lyra-ai-chat-message")], AIChatMessage);
644
+ //#endregion
645
+ //#region src/view/components/ai-chat-input.ts
646
+ var AIChatInput = class AIChatInput extends LitElement {
647
+ constructor(..._args) {
648
+ super(..._args);
649
+ this.value = "";
650
+ this.disabled = false;
651
+ this.busy = false;
652
+ this.hasProvider = true;
653
+ }
654
+ onInput(event) {
655
+ this.value = event.target.value;
656
+ this.dispatchEvent(new CustomEvent("input-change", {
657
+ detail: { value: this.value },
658
+ bubbles: true,
659
+ composed: true
660
+ }));
661
+ }
662
+ onKeyDown(event) {
663
+ if (event.key === "Enter" && !event.shiftKey) {
664
+ event.preventDefault();
665
+ this.send();
666
+ }
667
+ }
668
+ async send() {
669
+ if (!this.value.trim() || this.disabled || !this.hasProvider) return;
670
+ const messageValue = this.value;
671
+ this.value = "";
672
+ this.requestUpdate();
673
+ await this.updateComplete;
674
+ if (this.textareaElement) {
675
+ this.textareaElement.value = "";
676
+ this.textareaElement.focus();
677
+ }
678
+ this.dispatchEvent(new CustomEvent("send", {
679
+ detail: { value: messageValue },
680
+ bubbles: true,
681
+ composed: true
682
+ }));
683
+ }
684
+ cancel() {
685
+ this.dispatchEvent(new CustomEvent("cancel", {
686
+ bubbles: true,
687
+ composed: true
688
+ }));
689
+ }
690
+ render() {
691
+ return html`
692
+ <div class="input-container">
693
+ <div class="input-row">
694
+ <wa-textarea
695
+ placeholder="Type a message... (Enter to send, Shift+Enter for new line)"
696
+ size="small"
697
+ resize="auto"
698
+ rows="1"
699
+ .value="${this.value}"
700
+ ?disabled="${this.disabled || !this.hasProvider}"
701
+ @input="${this.onInput}"
702
+ @keydown="${this.onKeyDown}">
703
+ </wa-textarea>
704
+ ${when(this.busy, () => html`
705
+ <wa-button appearance="plain" size="small" @click="${this.cancel}">
706
+ <wa-icon name="stop" label="Stop"></wa-icon>
707
+ </wa-button>
708
+ `)}
709
+ </div>
710
+ </div>
711
+ `;
712
+ }
713
+ static {
714
+ this.styles = css`
715
+ :host { display: block; width: 100%; }
716
+ .input-container { margin-bottom: 0.25rem; margin-left: 0.25rem; }
717
+ .input-row { display: flex; gap: 0.5rem; align-items: flex-end; }
718
+ wa-textarea { flex: 1; min-width: 0; }
719
+ `;
720
+ }
721
+ };
722
+ _decorate([property({ type: String })], AIChatInput.prototype, "value", void 0);
723
+ _decorate([property({ type: Boolean })], AIChatInput.prototype, "disabled", void 0);
724
+ _decorate([property({ type: Boolean })], AIChatInput.prototype, "busy", void 0);
725
+ _decorate([property({ type: Boolean })], AIChatInput.prototype, "hasProvider", void 0);
726
+ _decorate([query("wa-textarea")], AIChatInput.prototype, "textareaElement", void 0);
727
+ AIChatInput = _decorate([customElement("lyra-ai-chat-input")], AIChatInput);
728
+ //#endregion
729
+ //#region src/view/components/ai-agent-response-group.ts
730
+ var AIAgentResponseGroup = class AIAgentResponseGroup extends LitElement {
731
+ copyToClipboard(text) {
732
+ navigator.clipboard.writeText(text).catch((err) => console.error("Failed to copy:", err));
733
+ }
734
+ renderStatusIcon(status) {
735
+ switch (status) {
736
+ case "streaming": return html`<wa-icon name="spinner" class="spinning"></wa-icon>`;
737
+ case "completed": return html`<wa-icon name="check-circle" class="status-success"></wa-icon>`;
738
+ case "error": return html`<wa-icon name="exclamation-circle" class="status-error"></wa-icon>`;
739
+ }
740
+ }
741
+ renderCard(agentInfo, message) {
742
+ if (!message) return html`
743
+ <div class="agent-card status-${agentInfo.status}">
744
+ <div class="agent-card-header">
745
+ <wa-icon name="${agentInfo.icon}" label="${agentInfo.label}"></wa-icon>
746
+ <span>${agentInfo.label}</span>
747
+ ${this.renderStatusIcon(agentInfo.status)}
748
+ </div>
749
+ <div class="agent-card-content waiting">Waiting for response...</div>
750
+ </div>
751
+ `;
752
+ return html`
753
+ <div class="agent-card status-${agentInfo.status}">
754
+ <div class="agent-card-header">
755
+ <wa-icon name="${agentInfo.icon}" label="${agentInfo.label}"></wa-icon>
756
+ <span>${agentInfo.label}</span>
757
+ ${this.renderStatusIcon(agentInfo.status)}
758
+ <div class="agent-card-actions">
759
+ <wa-button variant="neutral" appearance="plain" size="small" title="Copy"
760
+ @click="${() => this.copyToClipboard(message.content || "")}">
761
+ <wa-icon name="copy" label="Copy"></wa-icon>
762
+ </wa-button>
763
+ </div>
764
+ </div>
765
+ <div class="agent-card-content">
766
+ <lyra-ai-chat-message
767
+ .message="${message}"
768
+ .isStreaming="${agentInfo.status === "streaming"}"
769
+ .showHeader="${false}"
770
+ .messageIndex="${agentInfo.messageIndex}">
771
+ </lyra-ai-chat-message>
772
+ </div>
773
+ </div>
774
+ `;
775
+ }
776
+ render() {
777
+ if (!this.group) return html``;
778
+ const agents = Array.from(this.group.agents.values());
779
+ const completedCount = agents.filter((a) => a.status === "completed").length;
780
+ const streamingCount = agents.filter((a) => a.status === "streaming").length;
781
+ const errorCount = agents.filter((a) => a.status === "error").length;
782
+ const allDone = agents.length > 0 && completedCount + errorCount === agents.length;
783
+ return html`
784
+ <div class="agent-response-group">
785
+ ${when(!(agents.length === 1), () => html`
786
+ <div class="group-header">
787
+ <wa-icon name="robot" label="Multiple Agents"></wa-icon>
788
+ <span>Multiple Agents</span>
789
+ <span class="status-badge">
790
+ ${when(streamingCount > 0, () => html`<span class="streaming">${streamingCount} responding</span>`)}
791
+ ${when(allDone, () => html`<span class="done">All completed (${completedCount})</span>`)}
792
+ </span>
793
+ </div>
794
+ `)}
795
+ <div class="group-content">
796
+ ${repeat(agents, (a) => a.role, (agentInfo) => {
797
+ const message = agentInfo.message || (agentInfo.status === "streaming" && this.findStreamingMessage ? this.findStreamingMessage(agentInfo.role) : void 0);
798
+ return this.renderCard(agentInfo, message);
799
+ })}
800
+ </div>
801
+ </div>
802
+ `;
803
+ }
804
+ static {
805
+ this.styles = css`
806
+ :host { display: block; width: 100%; box-sizing: border-box; }
807
+
808
+ .agent-response-group {
809
+ display: flex;
810
+ flex-direction: column;
811
+ gap: 0.5rem;
812
+ width: 100%;
813
+ }
814
+
815
+ .group-header {
816
+ display: flex;
817
+ align-items: center;
818
+ gap: 0.5rem;
819
+ padding: 0.5rem 0.75rem;
820
+ background-color: var(--wa-color-surface-lowered);
821
+ border: solid var(--wa-border-width-s) var(--wa-color-surface-border);
822
+ font-weight: 500;
823
+ }
824
+
825
+ .status-badge {
826
+ display: flex;
827
+ gap: 0.5rem;
828
+ margin-left: auto;
829
+ font-size: 0.875rem;
830
+ }
831
+
832
+ .streaming { color: var(--wa-color-brand-50); }
833
+ .done { color: var(--wa-color-success-70); font-weight: 600; }
834
+
835
+ .group-content {
836
+ display: flex;
837
+ flex-direction: column;
838
+ gap: 0.5rem;
839
+ width: 100%;
840
+ }
841
+
842
+ .agent-card {
843
+ display: flex;
844
+ flex-direction: column;
845
+ border: solid var(--wa-border-width-s) var(--wa-color-surface-border);
846
+ background-color: var(--wa-color-surface-default);
847
+ }
848
+
849
+ .agent-card.status-streaming { border-color: var(--wa-color-brand-border-quiet); }
850
+ .agent-card.status-completed { border-color: var(--wa-color-success-border-quiet); }
851
+ .agent-card.status-error { border-color: var(--wa-color-danger-border-quiet); }
852
+
853
+ .agent-card-header {
854
+ display: flex;
855
+ align-items: center;
856
+ gap: 0.375rem;
857
+ padding: 0.375rem 0.5rem;
858
+ border-bottom: solid var(--wa-border-width-s) var(--wa-color-surface-border);
859
+ background-color: var(--wa-color-surface-lowered);
860
+ font-weight: 500;
861
+ font-size: 0.875rem;
862
+ }
863
+
864
+ .agent-card-actions { margin-left: auto; display: flex; gap: 0.25rem; }
865
+ .agent-card-content { padding: 0.375rem; }
866
+ .waiting { padding: 1rem; text-align: center; color: var(--wa-color-text-quiet); }
867
+
868
+ .spinning { animation: spin 1s linear infinite; }
869
+ .status-success { color: var(--wa-color-success-60); }
870
+ .status-error { color: var(--wa-color-danger-60); }
871
+
872
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
873
+ `;
874
+ }
875
+ };
876
+ _decorate([property({
877
+ type: Object,
878
+ attribute: false
879
+ })], AIAgentResponseGroup.prototype, "group", void 0);
880
+ _decorate([property({
881
+ type: Function,
882
+ attribute: false
883
+ })], AIAgentResponseGroup.prototype, "findStreamingMessage", void 0);
884
+ AIAgentResponseGroup = _decorate([customElement("lyra-ai-agent-response-group")], AIAgentResponseGroup);
885
+ //#endregion
886
+ //#region src/view/components/ai-tool-approval.ts
887
+ var AIToolApproval = class AIToolApproval extends LitElement {
888
+ constructor(..._args) {
889
+ super(..._args);
890
+ this.pendingApprovals = /* @__PURE__ */ new Map();
891
+ }
892
+ approve(approvalId, approval) {
893
+ this.dispatchEvent(new CustomEvent("approve", {
894
+ detail: {
895
+ approvalId,
896
+ approval
897
+ },
898
+ bubbles: true,
899
+ composed: true
900
+ }));
901
+ approval.resolve(true);
902
+ this.pendingApprovals.delete(approvalId);
903
+ this.requestUpdate();
904
+ }
905
+ deny(approvalId, approval) {
906
+ approval.resolve(false);
907
+ this.pendingApprovals.delete(approvalId);
908
+ this.requestUpdate();
909
+ }
910
+ formatArgs(argsStr) {
911
+ let parsed = {};
912
+ try {
913
+ parsed = JSON.parse(argsStr);
914
+ } catch {}
915
+ return Object.entries(parsed).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
916
+ }
917
+ render() {
918
+ if (this.pendingApprovals.size === 0) return html``;
919
+ return html`
920
+ <div class="approval-container">
921
+ ${Array.from(this.pendingApprovals.entries()).map(([id, approval]) => {
922
+ const toolCalls = approval.request.toolCalls;
923
+ const first = toolCalls[0];
924
+ return html`
925
+ <wa-details class="approval-item">
926
+ <span slot="summary" class="approval-summary">
927
+ <span>${toolCalls.length === 1 ? `AI wants to execute: ${first?.function.name}()` : `AI wants to execute ${toolCalls.length} tools`}</span>
928
+ <div class="approval-inline-actions">
929
+ <wa-button appearance="plain" size="small" variant="neutral"
930
+ @click="${(e) => {
931
+ e.stopPropagation();
932
+ this.deny(id, approval);
933
+ }}">
934
+ <wa-icon name="xmark" label="Deny"></wa-icon>
935
+ </wa-button>
936
+ <wa-button appearance="plain" size="small" variant="success"
937
+ @click="${async (e) => {
938
+ e.stopPropagation();
939
+ this.approve(id, approval);
940
+ }}">
941
+ <wa-icon name="check" label="Approve"></wa-icon>
942
+ </wa-button>
943
+ </div>
944
+ </span>
945
+ <div class="approval-detail">
946
+ <strong>${approval.role} wants to execute:</strong>
947
+ <ul class="tool-list">
948
+ ${repeat(toolCalls, (tc) => tc.id, (tc) => {
949
+ const argsStr = this.formatArgs(tc.function.arguments || "{}");
950
+ return html`
951
+ <li class="tool-item">
952
+ <label class="always-allow-label">
953
+ <wa-checkbox
954
+ ?checked="${approval.alwaysAllowSelections.get(tc.id) || false}"
955
+ @change="${(e) => {
956
+ approval.alwaysAllowSelections.set(tc.id, e.target.checked);
957
+ this.requestUpdate();
958
+ }}">
959
+ </wa-checkbox>
960
+ <span>Always allow</span>
961
+ </label>
962
+ <code>${tc.function.name}(${argsStr})</code>
963
+ </li>
964
+ `;
965
+ })}
966
+ </ul>
967
+ </div>
968
+ </wa-details>
969
+ `;
970
+ })}
971
+ </div>
972
+ `;
973
+ }
974
+ static {
975
+ this.styles = css`
976
+ :host { display: block; }
977
+
978
+ .approval-container {
979
+ display: flex;
980
+ flex-direction: column;
981
+ gap: 0.5rem;
982
+ padding: 0.75rem 1rem;
983
+ border-top: solid var(--wa-border-width-s) var(--wa-color-warning-border-normal);
984
+ background-color: var(--wa-color-warning-fill-quiet);
985
+ }
986
+
987
+ .approval-summary {
988
+ display: flex;
989
+ align-items: center;
990
+ justify-content: space-between;
991
+ gap: 1rem;
992
+ width: 100%;
993
+ }
994
+
995
+ .approval-inline-actions { display: flex; gap: 0.5rem; flex-shrink: 0; }
996
+
997
+ .approval-detail {
998
+ display: flex;
999
+ flex-direction: column;
1000
+ gap: 0.5rem;
1001
+ padding: 0.75rem 0;
1002
+ font-size: 0.875rem;
1003
+ }
1004
+
1005
+ .tool-list { margin: 0.5rem 0 0 1.5rem; padding: 0; list-style: disc; }
1006
+
1007
+ .tool-item {
1008
+ display: flex;
1009
+ align-items: center;
1010
+ gap: 0.5rem;
1011
+ margin: 0.5rem 0;
1012
+ }
1013
+
1014
+ .always-allow-label {
1015
+ display: flex;
1016
+ align-items: center;
1017
+ gap: 0.375rem;
1018
+ cursor: pointer;
1019
+ }
1020
+
1021
+ code {
1022
+ font-family: var(--wa-font-mono);
1023
+ font-size: 0.875rem;
1024
+ padding: 0.125rem 0.25rem;
1025
+ background-color: var(--wa-color-neutral-fill-subtle);
1026
+ border-radius: var(--wa-border-radius-s);
1027
+ }
1028
+ `;
1029
+ }
1030
+ };
1031
+ _decorate([property({
1032
+ type: Map,
1033
+ attribute: false
1034
+ })], AIToolApproval.prototype, "pendingApprovals", void 0);
1035
+ AIToolApproval = _decorate([customElement("lyra-ai-tool-approval")], AIToolApproval);
1036
+ //#endregion
1037
+ //#region src/view/components/ai-empty-state.ts
1038
+ var AIEmptyState = class AIEmptyState extends LitElement {
1039
+ constructor(..._args) {
1040
+ super(..._args);
1041
+ this.message = "No AI provider configured";
1042
+ this.hint = "Click the settings icon to configure an AI provider";
1043
+ }
1044
+ render() {
1045
+ return html`
1046
+ <div class="empty-state">
1047
+ <wa-icon name="robot" style="font-size: 3rem; opacity: 0.3;"></wa-icon>
1048
+ <p>${this.message}</p>
1049
+ <p class="hint">${this.hint}</p>
1050
+ </div>
1051
+ `;
1052
+ }
1053
+ static {
1054
+ this.styles = css`
1055
+ :host {
1056
+ display: flex;
1057
+ align-items: center;
1058
+ justify-content: center;
1059
+ width: 100%;
1060
+ height: 100%;
1061
+ }
1062
+
1063
+ .empty-state {
1064
+ display: flex;
1065
+ flex-direction: column;
1066
+ align-items: center;
1067
+ justify-content: center;
1068
+ padding: 2rem;
1069
+ text-align: center;
1070
+ color: var(--wa-color-text-quiet);
1071
+ }
1072
+
1073
+ .empty-state p { margin: 0.5rem 0; }
1074
+ .hint { font-size: 0.875rem; opacity: 0.7; }
1075
+ `;
1076
+ }
1077
+ };
1078
+ _decorate([property({ type: String })], AIEmptyState.prototype, "message", void 0);
1079
+ _decorate([property({ type: String })], AIEmptyState.prototype, "hint", void 0);
1080
+ AIEmptyState = _decorate([customElement("lyra-ai-empty-state")], AIEmptyState);
1081
+ //#endregion
1082
+ //#region src/view/task-progress-panel.ts
1083
+ var STATUS_ICON = {
1084
+ running: "spinner",
1085
+ completed: "check-circle",
1086
+ failed: "exclamation-circle",
1087
+ skipped: "forward",
1088
+ pending: "circle"
1089
+ };
1090
+ var STATUS_COLOR = {
1091
+ running: "var(--wa-color-brand-50)",
1092
+ completed: "var(--wa-color-success-60)",
1093
+ failed: "var(--wa-color-danger-60)",
1094
+ skipped: "var(--wa-color-neutral-40)",
1095
+ pending: "var(--wa-color-neutral-40)"
1096
+ };
1097
+ var AITaskProgressPanel = class AITaskProgressPanel extends LitElement {
1098
+ constructor(..._args) {
1099
+ super(..._args);
1100
+ this.expanded = true;
1101
+ }
1102
+ render() {
1103
+ if (!this.plan) return html``;
1104
+ const completedCount = this.plan.steps.filter((s) => s.status === "completed").length;
1105
+ const totalCount = this.plan.steps.length;
1106
+ return html`
1107
+ <div class="taspanel">
1108
+ <div class="panel-header" @click="${() => {
1109
+ this.expanded = !this.expanded;
1110
+ }}">
1111
+ <wa-icon name="diagram-project" label="Task Plan"></wa-icon>
1112
+ <span class="panel-title">Task Plan</span>
1113
+ <span class="progress-text">${completedCount}/${totalCount}</span>
1114
+ <wa-progress-bar value="${totalCount > 0 ? Math.round(completedCount / totalCount * 100) : 0}" class="progress-bar"></wa-progress-bar>
1115
+ <wa-icon name="${this.expanded ? "chevron-up" : "chevron-down"}" label="toggle"></wa-icon>
1116
+ </div>
1117
+ ${when(this.expanded, () => html`
1118
+ <div class="panel-body">
1119
+ ${repeat(this.plan.steps, (s) => s.id, (step) => html`
1120
+ <div class="step-row">
1121
+ <wa-icon
1122
+ name="${STATUS_ICON[step.status] ?? "circle"}"
1123
+ style="color: ${STATUS_COLOR[step.status] ?? "var(--wa-color-neutral-40)"}; ${step.status === "running" ? "animation: spin 1s linear infinite;" : ""}">
1124
+ </wa-icon>
1125
+ <div class="step-info">
1126
+ <span class="step-role">${step.role}</span>
1127
+ <span class="step-task">${step.subTask}</span>
1128
+ </div>
1129
+ ${when(step.revisions > 0, () => html`
1130
+ <span class="revisions-badge">${step.revisions} rev</span>
1131
+ `)}
1132
+ </div>
1133
+ `)}
1134
+ </div>
1135
+ `)}
1136
+ </div>
1137
+ `;
1138
+ }
1139
+ static {
1140
+ this.styles = css`
1141
+ :host { display: block; }
1142
+
1143
+ .taspanel {
1144
+ border: solid var(--wa-border-width-s) var(--wa-color-brand-border-quiet);
1145
+ border-radius: var(--wa-border-radius-m);
1146
+ background: var(--wa-color-surface-default);
1147
+ margin: 0.5rem 0;
1148
+ }
1149
+
1150
+ .panel-header {
1151
+ display: flex;
1152
+ align-items: center;
1153
+ gap: 0.5rem;
1154
+ padding: 0.5rem 0.75rem;
1155
+ cursor: pointer;
1156
+ user-select: none;
1157
+ }
1158
+
1159
+ .panel-title {
1160
+ font-weight: 500;
1161
+ flex: 0 0 auto;
1162
+ }
1163
+
1164
+ .progress-text {
1165
+ font-size: 0.8rem;
1166
+ color: var(--wa-color-text-quiet);
1167
+ }
1168
+
1169
+ .progress-bar {
1170
+ flex: 1;
1171
+ min-width: 60px;
1172
+ }
1173
+
1174
+ .panel-body {
1175
+ display: flex;
1176
+ flex-direction: column;
1177
+ gap: 0.25rem;
1178
+ padding: 0.5rem 0.75rem;
1179
+ border-top: solid var(--wa-border-width-s) var(--wa-color-neutral-border-subtle);
1180
+ }
1181
+
1182
+ .step-row {
1183
+ display: flex;
1184
+ align-items: flex-start;
1185
+ gap: 0.5rem;
1186
+ padding: 0.25rem 0;
1187
+ }
1188
+
1189
+ .step-info {
1190
+ display: flex;
1191
+ flex-direction: column;
1192
+ gap: 0.125rem;
1193
+ flex: 1;
1194
+ min-width: 0;
1195
+ }
1196
+
1197
+ .step-role {
1198
+ font-size: 0.75rem;
1199
+ font-weight: 600;
1200
+ color: var(--wa-color-text-quiet);
1201
+ text-transform: uppercase;
1202
+ }
1203
+
1204
+ .step-task {
1205
+ font-size: 0.85rem;
1206
+ white-space: nowrap;
1207
+ overflow: hidden;
1208
+ text-overflow: ellipsis;
1209
+ }
1210
+
1211
+ .revisions-badge {
1212
+ font-size: 0.7rem;
1213
+ padding: 0.1rem 0.3rem;
1214
+ background: var(--wa-color-warning-fill-quiet);
1215
+ border-radius: var(--wa-border-radius-s);
1216
+ color: var(--wa-color-warning-70);
1217
+ flex-shrink: 0;
1218
+ }
1219
+
1220
+ @keyframes spin {
1221
+ from { transform: rotate(0deg); }
1222
+ to { transform: rotate(360deg); }
1223
+ }
1224
+ `;
1225
+ }
1226
+ };
1227
+ _decorate([property({
1228
+ type: Object,
1229
+ attribute: false
1230
+ })], AITaskProgressPanel.prototype, "plan", void 0);
1231
+ _decorate([state()], AITaskProgressPanel.prototype, "expanded", void 0);
1232
+ AITaskProgressPanel = _decorate([customElement("lyra-ai-task-progress-panel")], AITaskProgressPanel);
1233
+ //#endregion
1234
+ //#region src/view/workspace-panel.ts
1235
+ var ARTIFACT_TYPE_ICON = {
1236
+ code: "code",
1237
+ json: "brackets-curly",
1238
+ "file-list": "list",
1239
+ plan: "diagram-project",
1240
+ review: "magnifying-glass",
1241
+ text: "file-lines"
1242
+ };
1243
+ var AIWorkspacePanel = class AIWorkspacePanel extends LitElement {
1244
+ constructor(..._args) {
1245
+ super(..._args);
1246
+ this.artifacts = [];
1247
+ this.expanded = false;
1248
+ }
1249
+ render() {
1250
+ if (this.artifacts.length === 0) return html``;
1251
+ return html`
1252
+ <div class="workspace-panel">
1253
+ <div class="panel-header" @click="${() => {
1254
+ this.expanded = !this.expanded;
1255
+ this.selectedArtifact = void 0;
1256
+ }}">
1257
+ <wa-icon name="folder-open" label="Workspace"></wa-icon>
1258
+ <span class="panel-title">Workspace</span>
1259
+ <span class="count-badge">${this.artifacts.length} artifact${this.artifacts.length !== 1 ? "s" : ""}</span>
1260
+ <wa-icon name="${this.expanded ? "chevron-up" : "chevron-down"}" label="toggle"></wa-icon>
1261
+ </div>
1262
+ ${when(this.expanded, () => html`
1263
+ <div class="panel-body">
1264
+ <div class="artifact-list">
1265
+ ${repeat(this.artifacts, (a) => a.id, (artifact) => html`
1266
+ <div
1267
+ class="artifact-item ${this.selectedArtifact?.id === artifact.id ? "selected" : ""}"
1268
+ @click="${() => {
1269
+ this.selectedArtifact = this.selectedArtifact?.id === artifact.id ? void 0 : artifact;
1270
+ }}">
1271
+ <wa-icon name="${ARTIFACT_TYPE_ICON[artifact.type] ?? "file-lines"}" label="${artifact.type}"></wa-icon>
1272
+ <div class="artifact-meta">
1273
+ <span class="artifact-id">${artifact.id}</span>
1274
+ <span class="artifact-producer">by ${artifact.producedBy}</span>
1275
+ </div>
1276
+ <span class="artifact-type">${artifact.type}</span>
1277
+ </div>
1278
+ ${when(this.selectedArtifact?.id === artifact.id, () => html`
1279
+ <div class="artifact-content">
1280
+ <pre>${artifact.content}</pre>
1281
+ </div>
1282
+ `)}
1283
+ `)}
1284
+ </div>
1285
+ </div>
1286
+ `)}
1287
+ </div>
1288
+ `;
1289
+ }
1290
+ static {
1291
+ this.styles = css`
1292
+ :host { display: block; }
1293
+
1294
+ .workspace-panel {
1295
+ border: solid var(--wa-border-width-s) var(--wa-color-neutral-border-subtle);
1296
+ border-radius: var(--wa-border-radius-m);
1297
+ background: var(--wa-color-surface-default);
1298
+ margin: 0.5rem 0;
1299
+ }
1300
+
1301
+ .panel-header {
1302
+ display: flex;
1303
+ align-items: center;
1304
+ gap: 0.5rem;
1305
+ padding: 0.5rem 0.75rem;
1306
+ cursor: pointer;
1307
+ user-select: none;
1308
+ }
1309
+
1310
+ .panel-title { font-weight: 500; }
1311
+
1312
+ .count-badge {
1313
+ font-size: 0.8rem;
1314
+ color: var(--wa-color-text-quiet);
1315
+ margin-left: auto;
1316
+ }
1317
+
1318
+ .panel-body {
1319
+ border-top: solid var(--wa-border-width-s) var(--wa-color-neutral-border-subtle);
1320
+ }
1321
+
1322
+ .artifact-list { display: flex; flex-direction: column; }
1323
+
1324
+ .artifact-item {
1325
+ display: flex;
1326
+ align-items: center;
1327
+ gap: 0.5rem;
1328
+ padding: 0.4rem 0.75rem;
1329
+ cursor: pointer;
1330
+ }
1331
+
1332
+ .artifact-item:hover { background: var(--wa-color-surface-lowered); }
1333
+ .artifact-item.selected { background: var(--wa-color-brand-fill-quiet); }
1334
+
1335
+ .artifact-meta {
1336
+ display: flex;
1337
+ flex-direction: column;
1338
+ flex: 1;
1339
+ min-width: 0;
1340
+ }
1341
+
1342
+ .artifact-id {
1343
+ font-size: 0.85rem;
1344
+ font-weight: 500;
1345
+ white-space: nowrap;
1346
+ overflow: hidden;
1347
+ text-overflow: ellipsis;
1348
+ }
1349
+
1350
+ .artifact-producer {
1351
+ font-size: 0.75rem;
1352
+ color: var(--wa-color-text-quiet);
1353
+ }
1354
+
1355
+ .artifact-type {
1356
+ font-size: 0.75rem;
1357
+ padding: 0.1rem 0.3rem;
1358
+ background: var(--wa-color-surface-lowered);
1359
+ border-radius: var(--wa-border-radius-s);
1360
+ }
1361
+
1362
+ .artifact-content {
1363
+ padding: 0.5rem 0.75rem;
1364
+ border-top: solid var(--wa-border-width-s) var(--wa-color-neutral-border-subtle);
1365
+ background: var(--wa-color-surface-lowered);
1366
+ }
1367
+
1368
+ .artifact-content pre {
1369
+ margin: 0;
1370
+ white-space: pre-wrap;
1371
+ word-break: breaword;
1372
+ font-size: 0.8rem;
1373
+ max-height: 200px;
1374
+ overflow-y: auto;
1375
+ }
1376
+ `;
1377
+ }
1378
+ };
1379
+ _decorate([property({
1380
+ type: Array,
1381
+ attribute: false
1382
+ })], AIWorkspacePanel.prototype, "artifacts", void 0);
1383
+ _decorate([state()], AIWorkspacePanel.prototype, "expanded", void 0);
1384
+ _decorate([state()], AIWorkspacePanel.prototype, "selectedArtifact", void 0);
1385
+ AIWorkspacePanel = _decorate([customElement("lyra-ai-workspace-panel")], AIWorkspacePanel);
1386
+ //#endregion
1387
+ //#region src/view/aiview.ts
1388
+ var LyraAIView = class LyraAIView extends LyraPart {
1389
+ constructor(..._args) {
1390
+ super(..._args);
1391
+ this.sessionManager = new SessionManager();
1392
+ this.streamManager = new StreamManager(() => {
1393
+ this.requestUpdate();
1394
+ if (this.scrollDebounceTimer) clearTimeout(this.scrollDebounceTimer);
1395
+ this.scrollDebounceTimer = setTimeout(async () => {
1396
+ await this.updateComplete;
1397
+ this.scrollToBottom();
1398
+ this.scrollDebounceTimer = void 0;
1399
+ }, 100);
1400
+ });
1401
+ this.providerManager = new ProviderManager(aiService);
1402
+ this.agentGroupManager = new AgentGroupManager();
1403
+ this.busy = false;
1404
+ this.inputValue = "";
1405
+ this.requireToolApproval = true;
1406
+ this.showHistory = false;
1407
+ this.currentArtifacts = [];
1408
+ this.pendingToolApprovals = /* @__PURE__ */ new Map();
1409
+ this.toolApprovalAllowlist = /* @__PURE__ */ new Set();
1410
+ }
1411
+ async doBeforeUI() {
1412
+ this.subscribe(TOPIC_AICONFIG_CHANGED, () => this.onAIConfigChanged());
1413
+ await this.sessionManager.load();
1414
+ if (!this.sessionManager.getActiveSession()) this.sessionManager.createSession();
1415
+ await this.providerManager.initialize();
1416
+ await this.loadSettings();
1417
+ this.requestUpdate();
1418
+ }
1419
+ async onAIConfigChanged() {
1420
+ await this.providerManager.initialize();
1421
+ await this.loadSettings();
1422
+ this.requestUpdate();
1423
+ }
1424
+ async loadSettings() {
1425
+ this.requireToolApproval = (await appSettings.get("aiConfig") || {}).requireToolApproval !== false;
1426
+ const allowlist = await this.providerManager.loadToolApprovalAllowlist();
1427
+ this.toolApprovalAllowlist = new Set(allowlist);
1428
+ }
1429
+ async scrollToBottom() {
1430
+ await this.updateComplete;
1431
+ const scroller = this.shadowRoot?.querySelector("wa-scroller.chat-messages");
1432
+ if (!scroller) return;
1433
+ const container = scroller.shadowRoot?.querySelector(".scroll-container");
1434
+ if (container) container.scrollTop = container.scrollHeight;
1435
+ else if (scroller.scrollTo) scroller.scrollTo({
1436
+ top: scroller.scrollHeight,
1437
+ behavior: "smooth"
1438
+ });
1439
+ }
1440
+ resetViewState() {
1441
+ this.inputValue = "";
1442
+ this.showHistory = false;
1443
+ this.currentTaskPlan = void 0;
1444
+ this.currentArtifacts = [];
1445
+ this.requestUpdate();
1446
+ }
1447
+ createNewSession() {
1448
+ this.sessionManager.createSession();
1449
+ this.resetViewState();
1450
+ }
1451
+ switchToSession(sessionId) {
1452
+ if (!this.sessionManager.switchToSession(sessionId)) return;
1453
+ this.resetViewState();
1454
+ }
1455
+ deletePastSession(sessionId) {
1456
+ this.sessionManager.deletePastSession(sessionId);
1457
+ this.requestUpdate();
1458
+ }
1459
+ async sendMessage() {
1460
+ const prompt = this.inputValue.trim();
1461
+ if (!prompt || this.busy) return;
1462
+ this.inputValue = "";
1463
+ await this.handlePrompt(prompt);
1464
+ }
1465
+ async handleResend(message) {
1466
+ if (!message || message.role !== "user") return;
1467
+ await this.handlePrompt(message.content);
1468
+ }
1469
+ cancelStream() {
1470
+ this.abortController?.abort();
1471
+ this.abortController = void 0;
1472
+ this.busy = false;
1473
+ this.streamManager.cancelUpdates();
1474
+ }
1475
+ async handlePrompt(prompt) {
1476
+ if (prompt.startsWith("/")) {
1477
+ await this.runCommand(prompt.substring(1));
1478
+ return;
1479
+ }
1480
+ const selectedProvider = this.providerManager.getSelectedProvider();
1481
+ if (!selectedProvider) {
1482
+ toastError("Please configure an AI provider in settings");
1483
+ return;
1484
+ }
1485
+ const session = this.sessionManager.getActiveSession();
1486
+ if (!session) return;
1487
+ const message = aiService.createMessage(prompt);
1488
+ this.sessionManager.addMessage(message);
1489
+ if (session.history.length === 1) this.sessionManager.setTitle(this.sessionManager.generateTitle(prompt));
1490
+ this.requestUpdate();
1491
+ await this.updateComplete;
1492
+ this.scrollToBottom();
1493
+ this.busy = true;
1494
+ this.currentTaskPlan = void 0;
1495
+ this.currentArtifacts = [];
1496
+ this.abortController = new AbortController();
1497
+ const streamingAgents = /* @__PURE__ */ new Map();
1498
+ const chatContext = { history: [...session.history] };
1499
+ const sessionId = session.id;
1500
+ const execContext = commandRegistry.createExecutionContext();
1501
+ const callContext = uiContext.createChild({ ...execContext });
1502
+ const contributions = aiService.getAgentContributions();
1503
+ if (contributions.length === 0) {
1504
+ toastError("No agents are registered.");
1505
+ this.busy = false;
1506
+ return;
1507
+ }
1508
+ const matchingAgents = contributions.filter((c) => !c.canHandle || c.canHandle({
1509
+ ...callContext.getProxy(),
1510
+ userPrompt: prompt
1511
+ })).sort((a, b) => (b.priority || 0) - (a.priority || 0));
1512
+ if (matchingAgents.length === 0) {
1513
+ toastError(`No agents available. Available: ${contributions.map((c) => c.role).join(", ")}`);
1514
+ this.busy = false;
1515
+ return;
1516
+ }
1517
+ const roles = matchingAgents.map((a) => a.role);
1518
+ const currentSession = this.sessionManager.getActiveSession();
1519
+ if (!currentSession) return;
1520
+ const groupId = this.agentGroupManager.createGroup(sessionId, currentSession.history.length - 1, message, roles, (role) => {
1521
+ const contrib = contributions.find((c) => c.role === role);
1522
+ return {
1523
+ label: contrib?.label || role,
1524
+ icon: contrib?.icon || "robot"
1525
+ };
1526
+ });
1527
+ taskService.runAsync("Calling AI assistant", async () => {
1528
+ return aiService.executeAgentWorkflow({
1529
+ chatContext,
1530
+ chatConfig: selectedProvider,
1531
+ callContext,
1532
+ execution: "parallel",
1533
+ stream: true,
1534
+ signal: this.abortController.signal,
1535
+ roles,
1536
+ requireToolApproval: this.requireToolApproval,
1537
+ onToolApprovalRequest: async (role, request) => {
1538
+ if (request.toolCalls.every((tc) => this.toolApprovalAllowlist.has(tc.function.name))) return true;
1539
+ return new Promise((resolve) => {
1540
+ const approvalId = `approval-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1541
+ const pending = {
1542
+ role,
1543
+ request,
1544
+ resolve,
1545
+ alwaysAllowSelections: /* @__PURE__ */ new Map()
1546
+ };
1547
+ this.pendingToolApprovals.set(approvalId, pending);
1548
+ this.requestUpdate();
1549
+ });
1550
+ },
1551
+ onAgentStart: async (role) => {
1552
+ const streamIndex = this.streamManager.createStreamingMessage(role);
1553
+ streamingAgents.set(role, streamIndex);
1554
+ this.agentGroupManager.updateAgentStatus(groupId, role, "streaming");
1555
+ this.requestUpdate();
1556
+ await this.updateComplete;
1557
+ this.scrollToBottom();
1558
+ },
1559
+ onToken: (role, token) => {
1560
+ const streamIndex = streamingAgents.get(role);
1561
+ if (streamIndex !== void 0) this.streamManager.updateStreamingMessage(streamIndex, token);
1562
+ },
1563
+ onAgentComplete: async (role, completedMessage) => {
1564
+ const targetSession = this.sessionManager.getActiveSession();
1565
+ if (!targetSession || targetSession.id !== sessionId) return;
1566
+ const streamIndex = streamingAgents.get(role);
1567
+ if (streamIndex !== void 0) {
1568
+ this.streamManager.completeStreamingMessage(streamIndex, completedMessage);
1569
+ const messageIndex = targetSession.history.length;
1570
+ this.sessionManager.addMessage(completedMessage);
1571
+ streamingAgents.delete(role);
1572
+ this.streamManager.removeStreamingMessage(streamIndex);
1573
+ this.agentGroupManager.updateAgentStatus(groupId, role, "completed", completedMessage, messageIndex);
1574
+ this.requestUpdate();
1575
+ await this.updateComplete;
1576
+ this.scrollToBottom();
1577
+ }
1578
+ },
1579
+ onAgentError: (role, error) => {
1580
+ const streamIndex = streamingAgents.get(role);
1581
+ if (streamIndex !== void 0) {
1582
+ this.streamManager.removeStreamingMessage(streamIndex);
1583
+ streamingAgents.delete(role);
1584
+ }
1585
+ this.agentGroupManager.updateAgentStatus(groupId, role, "error", {
1586
+ role,
1587
+ content: `Error: ${error.message}`
1588
+ });
1589
+ this.requestUpdate();
1590
+ toastError(`Agent ${role} error: ${error.message}`);
1591
+ }
1592
+ }).then(() => {
1593
+ this.agentGroupManager.clearCurrentGroup();
1594
+ });
1595
+ }).catch((error) => {
1596
+ if (error?.name !== "AbortError") toastError(`${error}`);
1597
+ }).finally(async () => {
1598
+ this.busy = false;
1599
+ this.abortController = void 0;
1600
+ this.streamManager.reset();
1601
+ this.agentGroupManager.clearCurrentGroup();
1602
+ this.requestUpdate();
1603
+ });
1604
+ }
1605
+ async runCommand(prompt) {
1606
+ const splits = prompt.trim().split(/\s+/);
1607
+ if (splits.length === 0) return;
1608
+ const commandId = splits.shift();
1609
+ const command = commandRegistry.getCommand(commandId);
1610
+ if (!command) {
1611
+ toastError(`Command not found: ${commandId}`);
1612
+ return;
1613
+ }
1614
+ const params = {};
1615
+ splits.forEach((c, i) => {
1616
+ if (command.parameters?.[i]) params[command.parameters[i].name] = c;
1617
+ });
1618
+ await commandRegistry.execute(commandId, commandRegistry.createExecutionContext(params));
1619
+ this.requestUpdate();
1620
+ }
1621
+ handleToolApproval(e) {
1622
+ const { approvalId, approval } = e.detail;
1623
+ Array.from(approval.alwaysAllowSelections.entries()).filter(([, v]) => v).map(([k]) => k).forEach((name) => this.toolApprovalAllowlist.add(name));
1624
+ this.pendingToolApprovals.delete(approvalId);
1625
+ this.requestUpdate();
1626
+ }
1627
+ renderMessage(message, index, isStreaming = false) {
1628
+ return html`
1629
+ <lyra-ai-chat-message
1630
+ .message="${message}"
1631
+ .isStreaming="${isStreaming}"
1632
+ .showHeader="${true}"
1633
+ .messageIndex="${index}"
1634
+ @resend="${(e) => this.handleResend(e.detail.message)}">
1635
+ </lyra-ai-chat-message>
1636
+ `;
1637
+ }
1638
+ renderToolbar() {
1639
+ const past = this.sessionManager.getPastSessions();
1640
+ return html`
1641
+ <span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.875rem;font-weight:500;padding:0 0.25rem;">${this.sessionManager.getActiveSession()?.title || "New Chat"}</span>
1642
+ <wa-button appearance="plain" size="small" title="New chat"
1643
+ @click="${() => this.createNewSession()}">
1644
+ <wa-icon name="plus" label="New chat"></wa-icon>
1645
+ </wa-button>
1646
+ ${past.length > 0 ? html`
1647
+ <wa-dropdown
1648
+ ?open="${this.showHistory}"
1649
+ @wa-after-hide="${() => {
1650
+ this.showHistory = false;
1651
+ }}"
1652
+ placement="bottom-start">
1653
+ <wa-button slot="trigger" appearance="plain" size="small" with-caret
1654
+ title="Chat history"
1655
+ @click="${() => {
1656
+ this.showHistory = !this.showHistory;
1657
+ }}">
1658
+ <wa-icon name="clock-rotate-left" label="History"></wa-icon>
1659
+ </wa-button>
1660
+ ${past.map((s) => html`
1661
+ <wa-dropdown-item @click="${() => this.switchToSession(s.id)}">
1662
+ <wa-icon name="message" label="Session" slot="icon"></wa-icon>
1663
+ <span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${s.title || "Unnamed Chat"}</span>
1664
+ <wa-button slot="details" appearance="plain" size="small" title="Delete"
1665
+ @click="${(e) => {
1666
+ e.stopPropagation();
1667
+ this.deletePastSession(s.id);
1668
+ }}">
1669
+ <wa-icon name="trash" label="Delete"></wa-icon>
1670
+ </wa-button>
1671
+ </wa-dropdown-item>
1672
+ `)}
1673
+ </wa-dropdown>
1674
+ ` : nothing}
1675
+ <lyra-command cmd="open_ai_config" icon="gear" title="AI Settings"></lyra-command>
1676
+ `;
1677
+ }
1678
+ renderContent() {
1679
+ const session = this.sessionManager.getActiveSession();
1680
+ const selectedProvider = this.providerManager.getSelectedProvider();
1681
+ return html`
1682
+ <div class="chat-container">
1683
+ <wa-scroller class="chat-messages" orientation="vertical">
1684
+ <div class="chat-content">
1685
+ ${when(!selectedProvider, () => html`
1686
+ <lyra-ai-empty-state
1687
+ message="No AI provider configured"
1688
+ hint='Click the settings icon below to configure an AI provider'>
1689
+ </lyra-ai-empty-state>
1690
+ `, () => when(!session || session.history.length === 0, () => html`
1691
+ <lyra-ai-empty-state message="How can I help you?" hint=""></lyra-ai-empty-state>
1692
+ `, () => html`
1693
+ ${session.history.map((message, idx) => {
1694
+ const group = this.agentGroupManager.findGroupForUserMessage(session.id, idx, message);
1695
+ if (group && message.role === "user") return html`
1696
+ <lyra-ai-chat-message
1697
+ .message="${message}"
1698
+ .isStreaming="${false}"
1699
+ .showHeader="${true}"
1700
+ .messageIndex="${idx}"
1701
+ @resend="${(e) => this.handleResend(e.detail.message)}">
1702
+ </lyra-ai-chat-message>
1703
+ <lyra-ai-agent-response-group
1704
+ .group="${group}"
1705
+ .findStreamingMessage="${(role) => this.streamManager.findStreamingMessage(role)}">
1706
+ </lyra-ai-agent-response-group>
1707
+ `;
1708
+ if (this.agentGroupManager.findGroupForMessage(session.id, message.role, idx)) return html``;
1709
+ return this.renderMessage(message, idx);
1710
+ })}
1711
+
1712
+ ${this.streamManager.getAllStreamingMessages().filter((m) => !this.agentGroupManager.getAllGroups().some((g) => g.sessionId === session.id && g.agents.has(m.message.role))).map((m) => this.renderMessage(m.message, -1, m.isStreaming))}
1713
+
1714
+ ${when(this.busy && this.streamManager.getAllStreamingMessages().length === 0, () => html`
1715
+ <div class="thinking-indicator">
1716
+ <wa-progress-ring indeterminate size="small"></wa-progress-ring>
1717
+ <span>Thinking…</span>
1718
+ </div>
1719
+ `)}
1720
+ `))}
1721
+
1722
+ ${when(this.currentTaskPlan, () => html`
1723
+ <lyra-ai-task-progress-panel .plan="${this.currentTaskPlan}"></lyra-ai-task-progress-panel>
1724
+ `)}
1725
+
1726
+ ${when(this.currentArtifacts.length > 0, () => html`
1727
+ <lyra-ai-workspace-panel .artifacts="${this.currentArtifacts}"></lyra-ai-workspace-panel>
1728
+ `)}
1729
+ </div>
1730
+ </wa-scroller>
1731
+
1732
+ ${when(this.pendingToolApprovals.size > 0, () => html`
1733
+ <lyra-ai-tool-approval
1734
+ .pendingApprovals="${this.pendingToolApprovals}"
1735
+ @approve="${(e) => this.handleToolApproval(e)}">
1736
+ </lyra-ai-tool-approval>
1737
+ `)}
1738
+
1739
+ <div class="input-area">
1740
+ <lyra-ai-chat-input
1741
+ .value="${this.inputValue}"
1742
+ .busy="${this.busy}"
1743
+ .disabled="${!selectedProvider}"
1744
+ .hasProvider="${!!selectedProvider}"
1745
+ @input-change="${(e) => {
1746
+ this.inputValue = e.detail.value;
1747
+ }}"
1748
+ @send="${(e) => {
1749
+ this.inputValue = e.detail.value;
1750
+ this.sendMessage();
1751
+ }}"
1752
+ @cancel="${() => this.cancelStream()}">
1753
+ </lyra-ai-chat-input>
1754
+ </div>
1755
+ </div>
1756
+ `;
1757
+ }
1758
+ static {
1759
+ this.styles = css`
1760
+ :host {
1761
+ display: flex;
1762
+ flex-direction: column;
1763
+ height: 100%;
1764
+ overflow: hidden;
1765
+ background: var(--wa-color-surface-default);
1766
+ }
1767
+
1768
+ .chat-container {
1769
+ display: flex;
1770
+ flex-direction: column;
1771
+ height: 100%;
1772
+ overflow: hidden;
1773
+ }
1774
+
1775
+ .chat-messages {
1776
+ flex: 1;
1777
+ overflow: hidden;
1778
+ }
1779
+
1780
+ .chat-content {
1781
+ display: flex;
1782
+ flex-direction: column;
1783
+ gap: 0.75rem;
1784
+ padding: 1rem;
1785
+ min-height: 100%;
1786
+ box-sizing: border-box;
1787
+ }
1788
+
1789
+ .thinking-indicator {
1790
+ display: flex;
1791
+ align-items: center;
1792
+ gap: 0.5rem;
1793
+ padding: 0.5rem 0.75rem;
1794
+ color: var(--wa-color-text-quiet);
1795
+ font-size: 0.875rem;
1796
+ }
1797
+
1798
+ .input-area {
1799
+ padding: 0.5rem;
1800
+ border-top: solid var(--wa-border-width-s) var(--wa-color-neutral-border-subtle);
1801
+ flex-shrink: 0;
1802
+ }
1803
+ `;
1804
+ }
1805
+ };
1806
+ _decorate([state()], LyraAIView.prototype, "busy", void 0);
1807
+ _decorate([state()], LyraAIView.prototype, "inputValue", void 0);
1808
+ _decorate([state()], LyraAIView.prototype, "requireToolApproval", void 0);
1809
+ _decorate([state()], LyraAIView.prototype, "showHistory", void 0);
1810
+ _decorate([state()], LyraAIView.prototype, "currentTaskPlan", void 0);
1811
+ _decorate([state()], LyraAIView.prototype, "currentArtifacts", void 0);
1812
+ _decorate([state()], LyraAIView.prototype, "pendingToolApprovals", void 0);
1813
+ LyraAIView = _decorate([customElement("lyra-aiview")], LyraAIView);
1814
+ //#endregion
1815
+ //#region src/view/token-usage.ts
1816
+ var LyraTokenUsage = class LyraTokenUsage extends LyraElement {
1817
+ constructor(..._args) {
1818
+ super(..._args);
1819
+ this.totalUsage = { ...EMPTY_USAGE };
1820
+ this.providerUsage = {};
1821
+ }
1822
+ connectedCallback() {
1823
+ super.connectedCallback();
1824
+ this.loadUsage();
1825
+ subscribe(TOPIC_AI_STREAM_COMPLETE, () => {
1826
+ this.loadUsage();
1827
+ });
1828
+ }
1829
+ async loadUsage() {
1830
+ this.totalUsage = await tokenUsageTracker.getTotalUsage();
1831
+ this.providerUsage = await tokenUsageTracker.getAllProviderUsage();
1832
+ this.requestUpdate();
1833
+ }
1834
+ formatNumber(num) {
1835
+ if (num >= 1e6) return (num / 1e6).toFixed(2) + "M";
1836
+ if (num >= 1e3) return (num / 1e3).toFixed(1) + "K";
1837
+ return num.toString();
1838
+ }
1839
+ async handleReset() {
1840
+ if (await confirmDialog("Reset all token usage statistics?")) {
1841
+ await tokenUsageTracker.reset();
1842
+ await this.loadUsage();
1843
+ }
1844
+ }
1845
+ renderStatItem(label, value) {
1846
+ return html`
1847
+ <div class="stat-item">
1848
+ <span class="stat-label">${label}</span>
1849
+ <span class="stat-value">${this.formatNumber(value)}</span>
1850
+ </div>
1851
+ `;
1852
+ }
1853
+ render() {
1854
+ if (this.totalUsage.totalTokens === 0) return html``;
1855
+ return html`
1856
+ <wa-dropdown placement="top-end" distance="8">
1857
+ <wa-button slot="trigger" appearance="plain" size="small" title="Token usage">
1858
+ <wa-icon name="database" label="Tokens" slot="start"></wa-icon>
1859
+ ${this.formatNumber(this.totalUsage.totalTokens)} tokens
1860
+ </wa-button>
1861
+
1862
+ <h3>Token Usage</h3>
1863
+
1864
+ <h6>Total</h6>
1865
+ <wa-dropdown-item>
1866
+ <span>All providers</span>
1867
+ <div class="stats-row">
1868
+ ${this.renderStatItem("Prompt", this.totalUsage.promptTokens)}
1869
+ ${this.renderStatItem("Completion", this.totalUsage.completionTokens)}
1870
+ ${this.renderStatItem("Total", this.totalUsage.totalTokens)}
1871
+ ${this.renderStatItem("Requests", this.totalUsage.requestCount)}
1872
+ </div>
1873
+ </wa-dropdown-item>
1874
+
1875
+ ${Object.keys(this.providerUsage).length > 0 ? html`
1876
+ <wa-divider></wa-divider>
1877
+ <h6>By Provider</h6>
1878
+ ${Object.entries(this.providerUsage).map(([name, usage]) => html`
1879
+ <wa-dropdown-item>
1880
+ <span class="provider-name">${name}</span>
1881
+ <div class="stats-row">
1882
+ ${this.renderStatItem("Prompt", usage.promptTokens)}
1883
+ ${this.renderStatItem("Completion", usage.completionTokens)}
1884
+ ${this.renderStatItem("Total", usage.totalTokens)}
1885
+ ${this.renderStatItem("Req", usage.requestCount)}
1886
+ </div>
1887
+ </wa-dropdown-item>
1888
+ `)}
1889
+ ` : ""}
1890
+
1891
+ <wa-divider></wa-divider>
1892
+ <wa-dropdown-item variant="danger" @click="${() => this.handleReset()}">
1893
+ <wa-icon name="trash" slot="icon"></wa-icon>
1894
+ Reset statistics
1895
+ </wa-dropdown-item>
1896
+ </wa-dropdown>
1897
+ `;
1898
+ }
1899
+ static {
1900
+ this.styles = css`
1901
+ :host { display: inline-block; }
1902
+
1903
+ wa-dropdown::part(menu) { min-width: 320px; max-width: 420px; }
1904
+
1905
+ h3 {
1906
+ padding: var(--wa-space-s) var(--wa-space-m);
1907
+ margin: 0;
1908
+ font-weight: 600;
1909
+ font-size: 0.95em;
1910
+ }
1911
+
1912
+ h6 {
1913
+ padding: var(--wa-space-xs) var(--wa-space-m);
1914
+ margin: 0;
1915
+ font-weight: 600;
1916
+ font-size: 0.85em;
1917
+ color: var(--wa-color-neutral-text-subtle);
1918
+ text-transform: uppercase;
1919
+ letter-spacing: 0.05em;
1920
+ }
1921
+
1922
+ .provider-name { font-weight: 500; }
1923
+
1924
+ .stats-row { display: flex; gap: var(--wa-space-m); font-size: 0.875rem; }
1925
+
1926
+ .stat-item {
1927
+ display: flex;
1928
+ flex-direction: column;
1929
+ align-items: flex-end;
1930
+ }
1931
+
1932
+ .stat-label { font-size: 0.8em; color: var(--wa-color-neutral-text-subtle); }
1933
+ .stat-value { font-weight: 600; }
1934
+ `;
1935
+ }
1936
+ };
1937
+ LyraTokenUsage = _decorate([customElement("lyra-token-usage")], LyraTokenUsage);
1938
+ //#endregion
1939
+ //#region src/view/components/ai-config-editor.ts
1940
+ var LyraAIConfigEditor = class LyraAIConfigEditor extends LyraPart {
1941
+ constructor(..._args) {
1942
+ super(..._args);
1943
+ this.providers = [];
1944
+ this.defaultProvider = "";
1945
+ this.hasChanges = false;
1946
+ this.availableModels = [];
1947
+ this.loadingModels = false;
1948
+ this.requireToolApproval = true;
1949
+ this.smartToolDetection = false;
1950
+ this.editingState = {};
1951
+ this.providerFactory = new ProviderFactory();
1952
+ }
1953
+ async doInitUI() {
1954
+ await this.loadConfig();
1955
+ subscribe(TOPIC_AICONFIG_CHANGED, () => this.loadConfig());
1956
+ subscribe(TOPIC_SETTINGS_CHANGED, () => this.loadConfig());
1957
+ }
1958
+ async loadConfig() {
1959
+ const config = await appSettings.get(KEY_AI_CONFIG);
1960
+ this.aiConfig = config;
1961
+ const contributed = contributionRegistry.getContributions(CID_CHAT_PROVIDERS).map((c) => c.provider);
1962
+ const configProviders = config?.providers || [];
1963
+ const existingNames = new Set(configProviders.map((p) => p.name));
1964
+ this.providers = [...configProviders, ...contributed.filter((p) => !existingNames.has(p.name))];
1965
+ this.defaultProvider = config?.defaultProvider || "";
1966
+ this.requireToolApproval = config?.requireToolApproval !== false;
1967
+ this.smartToolDetection = config?.smartToolDetection !== void 0 ? config.smartToolDetection : false;
1968
+ this.editingState = {};
1969
+ this.hasChanges = false;
1970
+ this.markDirty(false);
1971
+ }
1972
+ getEditValue(index, field) {
1973
+ const editing = this.editingState[index];
1974
+ if (editing && field in editing) return editing[field] ?? "";
1975
+ const provider = this.providers[index];
1976
+ return provider ? provider[field] ?? "" : "";
1977
+ }
1978
+ setEditValue(index, field, value) {
1979
+ this.editingState = {
1980
+ ...this.editingState,
1981
+ [index]: {
1982
+ ...this.editingState[index] || {},
1983
+ [field]: value
1984
+ }
1985
+ };
1986
+ this.providers = this.providers.map((p, i) => i === index ? {
1987
+ ...p,
1988
+ [field]: value
1989
+ } : p);
1990
+ this.markDirtyAndUpdate();
1991
+ }
1992
+ markDirtyAndUpdate() {
1993
+ this.hasChanges = true;
1994
+ this.markDirty(true);
1995
+ }
1996
+ async fetchModels(index) {
1997
+ const provider = this.providers[index];
1998
+ if (!provider) return;
1999
+ this.loadingModels = true;
2000
+ this.availableModels = [];
2001
+ try {
2002
+ const instance = this.providerFactory.getProvider(provider);
2003
+ if (instance.getAvailableModels) {
2004
+ const models = await instance.getAvailableModels(provider);
2005
+ this.availableModels = Array.isArray(models) ? models : [];
2006
+ }
2007
+ } finally {
2008
+ this.loadingModels = false;
2009
+ }
2010
+ }
2011
+ async saveConfig() {
2012
+ const updatedConfig = {
2013
+ ...this.aiConfig ?? {},
2014
+ defaultProvider: this.defaultProvider,
2015
+ providers: this.providers,
2016
+ requireToolApproval: this.requireToolApproval,
2017
+ smartToolDetection: this.smartToolDetection
2018
+ };
2019
+ await appSettings.set(KEY_AI_CONFIG, updatedConfig);
2020
+ this.aiConfig = updatedConfig;
2021
+ this.hasChanges = false;
2022
+ this.markDirty(false);
2023
+ }
2024
+ async save() {
2025
+ if (!this.hasChanges) return;
2026
+ await this.saveConfig();
2027
+ }
2028
+ addProvider() {
2029
+ this.providers = [...this.providers, {
2030
+ name: "new-provider",
2031
+ model: "",
2032
+ apiKey: "",
2033
+ chatApiEndpoint: ""
2034
+ }];
2035
+ this.markDirtyAndUpdate();
2036
+ }
2037
+ async deleteProvider(index) {
2038
+ const provider = this.providers[index];
2039
+ if (!await confirmDialog(`Delete provider "${provider.name}"?`)) return;
2040
+ if (this.defaultProvider === provider.name) this.defaultProvider = "";
2041
+ this.providers = this.providers.filter((_, i) => i !== index);
2042
+ this.markDirtyAndUpdate();
2043
+ }
2044
+ renderProviderField(index, field, type = "text") {
2045
+ const value = this.getEditValue(index, field);
2046
+ return html`
2047
+ <wa-input
2048
+ type="${type}"
2049
+ ?password-toggle="${type === "password"}"
2050
+ .value="${value}"
2051
+ @input="${(e) => this.setEditValue(index, field, e.target.value)}">
2052
+ </wa-input>
2053
+ `;
2054
+ }
2055
+ renderContent() {
2056
+ return html`
2057
+ <div class="editor">
2058
+ <div class="editor-header">
2059
+ <h2>AI Providers</h2>
2060
+ <wa-button variant="brand" appearance="filled" @click="${this.addProvider}">
2061
+ Add Provider
2062
+ </wa-button>
2063
+ </div>
2064
+
2065
+ ${when(this.providers.length === 0, () => html`
2066
+ <div class="empty-state"><p>No providers configured.</p></div>
2067
+ `, () => html`
2068
+ <div class="providers-list">
2069
+ ${repeat(this.providers, (_, i) => i, (provider, index) => html`
2070
+ <div class="provider-card">
2071
+ <div class="provider-card-header ${this.defaultProvider === provider.name ? "is-default" : ""}">
2072
+ <span class="provider-name">${provider.name}</span>
2073
+ ${this.defaultProvider === provider.name ? html`<span class="default-badge">Default</span>` : html`<wa-button appearance="plain" size="small" title="Set as default"
2074
+ @click="${() => {
2075
+ this.defaultProvider = provider.name;
2076
+ this.markDirtyAndUpdate();
2077
+ }}">
2078
+ Set default
2079
+ </wa-button>`}
2080
+ <wa-button variant="danger" appearance="plain" size="small"
2081
+ @click="${() => this.deleteProvider(index)}">
2082
+ Delete
2083
+ </wa-button>
2084
+ </div>
2085
+ <div class="provider-fields">
2086
+ <div class="field-row">
2087
+ <label>Name</label>
2088
+ ${this.renderProviderField(index, "name")}
2089
+ </div>
2090
+ <div class="field-row">
2091
+ <label>Model</label>
2092
+ <div class="model-row">
2093
+ ${this.renderProviderField(index, "model")}
2094
+ <wa-button appearance="plain" size="small"
2095
+ @click="${async () => {
2096
+ await this.fetchModels(index);
2097
+ }}"
2098
+ title="Fetch available models">
2099
+ <wa-icon name="refresh" label="Refresh"></wa-icon>
2100
+ </wa-button>
2101
+ </div>
2102
+ ${when(this.loadingModels, () => html`
2103
+ <wa-progress-ring indeterminate size="small"></wa-progress-ring>
2104
+ `)}
2105
+ ${when(this.availableModels.length > 0, () => html`
2106
+ <wa-dropdown
2107
+ @wa-select="${(e) => {
2108
+ if (e.detail.item?.value) this.setEditValue(index, "model", e.detail.item.value);
2109
+ }}">
2110
+ <wa-button slot="trigger" size="small" appearance="plain" with-caret>
2111
+ Select model
2112
+ </wa-button>
2113
+ ${this.availableModels.map((m) => html`
2114
+ <wa-dropdown-item value="${m.id}">${m.name || m.id}</wa-dropdown-item>
2115
+ `)}
2116
+ </wa-dropdown>
2117
+ `)}
2118
+ </div>
2119
+ <div class="field-row">
2120
+ <label>API Endpoint</label>
2121
+ ${this.renderProviderField(index, "chatApiEndpoint")}
2122
+ </div>
2123
+ <div class="field-row">
2124
+ <label>API Key</label>
2125
+ ${this.renderProviderField(index, "apiKey", "password")}
2126
+ </div>
2127
+ </div>
2128
+ </div>
2129
+ `)}
2130
+ </div>
2131
+ `)}
2132
+
2133
+ <div class="settings-section">
2134
+ <h3>Tool Settings</h3>
2135
+ <wa-checkbox
2136
+ ?checked="${this.requireToolApproval}"
2137
+ @change="${(e) => {
2138
+ this.requireToolApproval = e.target.checked;
2139
+ this.markDirtyAndUpdate();
2140
+ }}">
2141
+ Require approval before executing tools
2142
+ </wa-checkbox>
2143
+ <wa-checkbox
2144
+ ?checked="${this.smartToolDetection}"
2145
+ @change="${(e) => {
2146
+ this.smartToolDetection = e.target.checked;
2147
+ this.markDirtyAndUpdate();
2148
+ }}">
2149
+ Smart tool detection (use ML to detect when tools are needed)
2150
+ </wa-checkbox>
2151
+ </div>
2152
+ </div>
2153
+ `;
2154
+ }
2155
+ static {
2156
+ this.styles = css`
2157
+ :host { display: block; height: 100%; overflow: auto; }
2158
+
2159
+ .editor {
2160
+ display: flex;
2161
+ flex-direction: column;
2162
+ gap: 1.5rem;
2163
+ padding: 1rem;
2164
+ }
2165
+
2166
+ .editor-header {
2167
+ display: flex;
2168
+ justify-content: space-between;
2169
+ align-items: center;
2170
+ }
2171
+
2172
+ .editor-header h2 { margin: 0; font-size: 1.25rem; }
2173
+
2174
+ .providers-list { display: flex; flex-direction: column; gap: 1rem; }
2175
+
2176
+ .provider-card {
2177
+ border: solid var(--wa-border-width-s) var(--wa-color-neutral-border-loud);
2178
+ border-radius: var(--wa-border-radius-m);
2179
+ overflow: hidden;
2180
+ }
2181
+
2182
+ .provider-card-header {
2183
+ display: flex;
2184
+ align-items: center;
2185
+ gap: 0.75rem;
2186
+ padding: 0.5rem 0.75rem;
2187
+ background: var(--wa-color-surface-lowered);
2188
+ border-bottom: solid var(--wa-border-width-s) var(--wa-color-neutral-border-subtle);
2189
+ }
2190
+
2191
+ .provider-card-header.is-default {
2192
+ background: var(--wa-color-brand-fill-quiet);
2193
+ border-bottom-color: var(--wa-color-brand-border-quiet);
2194
+ }
2195
+
2196
+ .default-badge {
2197
+ font-size: 0.75rem;
2198
+ font-weight: 600;
2199
+ padding: 0.1rem 0.4rem;
2200
+ background: var(--wa-color-brand-fill-loud);
2201
+ color: var(--wa-color-brand-on-loud);
2202
+ border-radius: var(--wa-border-radius-s);
2203
+ text-transform: uppercase;
2204
+ letter-spacing: 0.04em;
2205
+ }
2206
+
2207
+ .provider-name {
2208
+ font-weight: 500;
2209
+ flex: 1;
2210
+ }
2211
+
2212
+ .provider-fields {
2213
+ display: flex;
2214
+ flex-direction: column;
2215
+ gap: 0.75rem;
2216
+ padding: 0.75rem;
2217
+ }
2218
+
2219
+ .field-row {
2220
+ display: grid;
2221
+ grid-template-columns: 120px 1fr;
2222
+ align-items: start;
2223
+ gap: 0.5rem;
2224
+ }
2225
+
2226
+ .field-row label {
2227
+ font-size: 0.875rem;
2228
+ color: var(--wa-color-text-quiet);
2229
+ padding-top: 0.4rem;
2230
+ }
2231
+
2232
+ .model-row { display: flex; gap: 0.25rem; align-items: center; }
2233
+ .model-row wa-input { flex: 1; }
2234
+
2235
+ .settings-section {
2236
+ display: flex;
2237
+ flex-direction: column;
2238
+ gap: 0.75rem;
2239
+ padding-top: 1rem;
2240
+ border-top: solid var(--wa-border-width-s) var(--wa-color-neutral-border-subtle);
2241
+ }
2242
+
2243
+ .settings-section h3 { margin: 0 0 0.5rem 0; font-size: 1rem; }
2244
+
2245
+ .empty-state {
2246
+ display: flex;
2247
+ justify-content: center;
2248
+ padding: 3rem;
2249
+ color: var(--wa-color-text-subtle);
2250
+ }
2251
+ `;
2252
+ }
2253
+ };
2254
+ _decorate([property({ attribute: false })], LyraAIConfigEditor.prototype, "input", void 0);
2255
+ _decorate([state()], LyraAIConfigEditor.prototype, "providers", void 0);
2256
+ _decorate([state()], LyraAIConfigEditor.prototype, "defaultProvider", void 0);
2257
+ _decorate([state()], LyraAIConfigEditor.prototype, "hasChanges", void 0);
2258
+ _decorate([state()], LyraAIConfigEditor.prototype, "availableModels", void 0);
2259
+ _decorate([state()], LyraAIConfigEditor.prototype, "loadingModels", void 0);
2260
+ _decorate([state()], LyraAIConfigEditor.prototype, "requireToolApproval", void 0);
2261
+ _decorate([state()], LyraAIConfigEditor.prototype, "smartToolDetection", void 0);
2262
+ _decorate([state()], LyraAIConfigEditor.prototype, "editingState", void 0);
2263
+ LyraAIConfigEditor = _decorate([customElement("lyra-ai-config-editor")], LyraAIConfigEditor);
2264
+ //#endregion
2265
+ //#region src/ai-system-extension.ts
2266
+ contributionRegistry.registerContribution(SIDEBAR_AUXILIARY, {
2267
+ name: "aiview",
2268
+ label: "AI Assistant",
2269
+ icon: "robot",
2270
+ component: (id) => html`<lyra-aiview id="${id}"></lyra-aiview>`
2271
+ });
2272
+ contributionRegistry.registerContribution(CID_AGENTS, {
2273
+ label: "App Support",
2274
+ description: "General-purpose assistant that can answer questions and execute app commands",
2275
+ role: "appsupport",
2276
+ priority: 100,
2277
+ icon: "question-circle",
2278
+ sysPrompt: general_assistant_prompt_default,
2279
+ tools: async () => {
2280
+ return {
2281
+ enabled: true,
2282
+ smartToolDetection: (await appSettings.get("aiConfig"))?.smartToolDetection ?? false
2283
+ };
2284
+ }
2285
+ });
2286
+ contributionRegistry.registerContribution(TOOLBAR_BOTTOM, {
2287
+ target: TOOLBAR_BOTTOM,
2288
+ label: "Token Usage",
2289
+ component: "<lyra-token-usage></lyra-token-usage>"
2290
+ });
2291
+ editorRegistry.registerEditorInputHandler({
2292
+ editorId: "system.ai-config-editor",
2293
+ label: "AI Config",
2294
+ ranking: 1e3,
2295
+ canHandle: (input) => input.key === ".system.ai-config",
2296
+ handle: async (input) => {
2297
+ input.component = (id) => html`<lyra-ai-config-editor id="${id}" .input=${input}></lyra-ai-config-editor>`;
2298
+ return input;
2299
+ }
2300
+ });
2301
+ registerAll({
2302
+ command: {
2303
+ id: "open_ai_config",
2304
+ name: "Open AI Configuration",
2305
+ description: "Open the AI system configuration editor",
2306
+ parameters: []
2307
+ },
2308
+ handler: { execute: (_context) => {
2309
+ editorRegistry.loadEditor({
2310
+ title: "AI Settings",
2311
+ data: {},
2312
+ key: ".system.ai-config",
2313
+ icon: "robot",
2314
+ state: {}
2315
+ }).then();
2316
+ } },
2317
+ contribution: {
2318
+ target: TOOLBAR_MAIN_RIGHT,
2319
+ icon: "robot",
2320
+ label: "AI Config"
2321
+ }
2322
+ });
2323
+ rootContext.put("aiService", aiService);
2324
+ //#endregion
2325
+
2326
+ //# sourceMappingURL=ai-system-extension-DnrA-ECv.js.map