copilot-liku-cli 0.0.3 → 0.0.8

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 (46) hide show
  1. package/QUICKSTART.md +24 -0
  2. package/README.md +85 -33
  3. package/package.json +23 -14
  4. package/scripts/postinstall.js +63 -0
  5. package/src/cli/commands/window.js +66 -0
  6. package/src/main/agents/base-agent.js +15 -7
  7. package/src/main/agents/builder.js +211 -0
  8. package/src/main/agents/index.js +7 -4
  9. package/src/main/agents/orchestrator.js +13 -0
  10. package/src/main/agents/producer.js +891 -0
  11. package/src/main/agents/researcher.js +78 -0
  12. package/src/main/agents/state-manager.js +134 -2
  13. package/src/main/agents/verifier.js +201 -0
  14. package/src/main/ai-service.js +349 -35
  15. package/src/main/index.js +702 -113
  16. package/src/main/inspect-service.js +24 -1
  17. package/src/main/python-bridge.js +395 -0
  18. package/src/main/system-automation.js +876 -131
  19. package/src/main/ui-automation/core/ui-provider.js +99 -0
  20. package/src/main/ui-automation/core/uia-host.js +214 -0
  21. package/src/main/ui-automation/index.js +30 -0
  22. package/src/main/ui-automation/interactions/element-click.js +6 -6
  23. package/src/main/ui-automation/interactions/high-level.js +28 -6
  24. package/src/main/ui-automation/interactions/index.js +21 -0
  25. package/src/main/ui-automation/interactions/pattern-actions.js +236 -0
  26. package/src/main/ui-automation/window/index.js +6 -0
  27. package/src/main/ui-automation/window/manager.js +173 -26
  28. package/src/main/ui-watcher.js +401 -58
  29. package/src/main/visual-awareness.js +18 -1
  30. package/src/native/windows-uia/Program.cs +89 -0
  31. package/src/native/windows-uia/build.ps1 +24 -0
  32. package/src/native/windows-uia-dotnet/Program.cs +920 -0
  33. package/src/native/windows-uia-dotnet/WindowsUIA.csproj +11 -0
  34. package/src/native/windows-uia-dotnet/build.ps1 +24 -0
  35. package/src/renderer/chat/chat.js +915 -671
  36. package/src/renderer/chat/index.html +2 -4
  37. package/src/renderer/chat/preload.js +8 -1
  38. package/src/renderer/overlay/overlay.js +157 -8
  39. package/src/renderer/overlay/preload.js +4 -0
  40. package/src/shared/inspect-types.js +82 -6
  41. package/ARCHITECTURE.md +0 -411
  42. package/CONFIGURATION.md +0 -302
  43. package/CONTRIBUTING.md +0 -225
  44. package/ELECTRON_README.md +0 -121
  45. package/PROJECT_STATUS.md +0 -229
  46. package/TESTING.md +0 -274
@@ -1,671 +1,915 @@
1
- // ===== STATE =====
2
- let currentMode = 'passive';
3
- let currentProvider = 'copilot';
4
- let currentModel = 'gpt-4o';
5
- let totalTokens = 0;
6
- let messages = [];
7
- let contextItems = [];
8
- let pendingActions = null;
9
-
10
- // ===== ELEMENTS =====
11
- const chatHistory = document.getElementById('chat-history');
12
- const messageInput = document.getElementById('message-input');
13
- const sendButton = document.getElementById('send-button');
14
- const passiveBtn = document.getElementById('passive-btn');
15
- const selectionBtn = document.getElementById('selection-btn');
16
- const minimizeBtn = document.getElementById('minimize-btn');
17
- const closeBtn = document.getElementById('close-btn');
18
- const captureBtn = document.getElementById('capture-btn');
19
- const contextPanel = document.getElementById('context-panel');
20
- const contextHeader = document.getElementById('context-header');
21
- const contextContent = document.getElementById('context-content');
22
- const contextCount = document.getElementById('context-count');
23
- const providerSelect = document.getElementById('provider-select');
24
- const modelSelect = document.getElementById('model-select');
25
- const authStatus = document.getElementById('auth-status');
26
- const tokenCount = document.getElementById('token-count');
27
-
28
- // ===== TOKEN ESTIMATION =====
29
- // Rough estimate: ~4 chars per token for English text
30
- function estimateTokens(text) {
31
- return Math.ceil(text.length / 4);
32
- }
33
-
34
- function updateTokenCount(additionalTokens = 0) {
35
- totalTokens += additionalTokens;
36
- if (tokenCount) {
37
- tokenCount.textContent = `${totalTokens.toLocaleString()} tokens`;
38
- }
39
- }
40
-
41
- function resetTokenCount() {
42
- totalTokens = 0;
43
- updateTokenCount();
44
- }
45
-
46
- // ===== AUTH STATUS =====
47
- function updateAuthStatus(status, provider) {
48
- if (!authStatus) return;
49
-
50
- authStatus.className = 'status-badge';
51
-
52
- switch (status) {
53
- case 'connected':
54
- authStatus.classList.add('connected');
55
- authStatus.textContent = `${getProviderName(provider)} Connected`;
56
- break;
57
- case 'pending':
58
- authStatus.classList.add('pending');
59
- authStatus.textContent = 'Authenticating...';
60
- break;
61
- case 'error':
62
- authStatus.classList.add('disconnected');
63
- authStatus.textContent = 'Auth Error';
64
- break;
65
- default:
66
- authStatus.classList.add('disconnected');
67
- authStatus.textContent = 'Not Connected';
68
- }
69
- }
70
-
71
- function getProviderName(provider) {
72
- const names = {
73
- copilot: 'Copilot',
74
- openai: 'OpenAI',
75
- anthropic: 'Anthropic',
76
- ollama: 'Ollama'
77
- };
78
- return names[provider] || provider;
79
- }
80
-
81
- // ===== PROVIDER FUNCTIONS =====
82
- function setProvider(provider) {
83
- currentProvider = provider;
84
- if (window.electronAPI.setProvider) {
85
- window.electronAPI.setProvider(provider);
86
- }
87
- // Also send as a command for backward compatibility
88
- window.electronAPI.sendMessage(`/provider ${provider}`);
89
- addMessage(`Switched to ${getProviderName(provider)}`, 'system');
90
-
91
- // Show/hide model selector based on provider
92
- updateModelSelector(provider);
93
-
94
- // Check auth status for new provider
95
- checkProviderAuth(provider);
96
- }
97
-
98
- // ===== MODEL FUNCTIONS =====
99
- function setModel(model) {
100
- currentModel = model;
101
- // Send model change command
102
- window.electronAPI.sendMessage(`/model ${model}`);
103
- }
104
-
105
- function updateModelSelector(provider) {
106
- if (!modelSelect) return;
107
-
108
- // Only show model selector for Copilot
109
- modelSelect.style.display = provider === 'copilot' ? 'block' : 'none';
110
- }
111
-
112
- // ===== MESSAGE FUNCTIONS =====
113
- function addMessage(text, type = 'agent', timestamp = Date.now(), extra = {}) {
114
- const emptyState = chatHistory.querySelector('.empty-state');
115
- if (emptyState) emptyState.remove();
116
-
117
- const messageEl = document.createElement('div');
118
- messageEl.className = `message ${type}`;
119
- if (extra.subtype) messageEl.classList.add(extra.subtype);
120
-
121
- const textEl = document.createElement('div');
122
- textEl.textContent = text;
123
- messageEl.appendChild(textEl);
124
-
125
- const timestampEl = document.createElement('div');
126
- timestampEl.className = 'timestamp';
127
- timestampEl.textContent = new Date(timestamp).toLocaleTimeString();
128
- messageEl.appendChild(timestampEl);
129
-
130
- chatHistory.appendChild(messageEl);
131
- chatHistory.scrollTop = chatHistory.scrollHeight;
132
-
133
- messages.push({ text, type, timestamp, ...extra });
134
-
135
- // Track tokens for user and agent messages
136
- if (type === 'user' || type === 'agent') {
137
- updateTokenCount(estimateTokens(text));
138
- }
139
- }
140
-
141
- function sendMessage() {
142
- const text = messageInput.value.trim();
143
- if (!text) return;
144
-
145
- addMessage(text, 'user');
146
- window.electronAPI.sendMessage(text);
147
-
148
- messageInput.value = '';
149
- messageInput.style.height = 'auto';
150
- }
151
-
152
- // ===== MODE FUNCTIONS =====
153
- function updateModeDisplay() {
154
- passiveBtn.classList.toggle('active', currentMode === 'passive');
155
- selectionBtn.classList.toggle('active', currentMode === 'selection');
156
- }
157
-
158
- function setMode(mode) {
159
- currentMode = mode;
160
- window.electronAPI.setMode(mode);
161
- updateModeDisplay();
162
-
163
- if (mode === 'selection') {
164
- addMessage('Selection mode active. Click dots on overlay or scroll to zoom.', 'system');
165
- } else {
166
- addMessage('Passive mode. Overlay is click-through.', 'system');
167
- }
168
- }
169
-
170
- // ===== CONTEXT PANEL FUNCTIONS =====
171
- function addContextItem(data) {
172
- contextItems.push(data);
173
- updateContextPanel();
174
- }
175
-
176
- function updateContextPanel() {
177
- contextCount.textContent = contextItems.length;
178
- contextContent.innerHTML = '';
179
-
180
- contextItems.forEach((item) => {
181
- const itemEl = document.createElement('div');
182
- itemEl.className = 'context-item';
183
- itemEl.innerHTML = `
184
- <span class="dot-marker"></span>
185
- <span>${item.label}</span>
186
- <span class="coords">(${item.x}, ${item.y})</span>
187
- `;
188
- contextContent.appendChild(itemEl);
189
- });
190
-
191
- if (contextItems.length > 0) {
192
- contextPanel.classList.add('expanded');
193
- }
194
- }
195
-
196
- function toggleContextPanel() {
197
- contextPanel.classList.toggle('expanded');
198
- }
199
-
200
- // ===== WINDOW CONTROLS =====
201
- minimizeBtn.addEventListener('click', () => {
202
- window.electronAPI.minimizeWindow();
203
- });
204
-
205
- closeBtn.addEventListener('click', () => {
206
- window.electronAPI.hideWindow();
207
- });
208
-
209
- // ===== CAPTURE FUNCTION =====
210
- captureBtn.addEventListener('click', () => {
211
- addMessage('Initiating screen capture...', 'system', Date.now(), { subtype: 'capture' });
212
- window.electronAPI.captureScreen();
213
- });
214
-
215
- // ===== EVENT LISTENERS =====
216
- sendButton.addEventListener('click', sendMessage);
217
-
218
- messageInput.addEventListener('keydown', (e) => {
219
- if (e.key === 'Enter' && !e.shiftKey) {
220
- e.preventDefault();
221
- sendMessage();
222
- }
223
- });
224
-
225
- // Auto-resize textarea
226
- messageInput.addEventListener('input', () => {
227
- messageInput.style.height = 'auto';
228
- messageInput.style.height = Math.min(messageInput.scrollHeight, 120) + 'px';
229
- });
230
-
231
- passiveBtn.addEventListener('click', () => setMode('passive'));
232
- selectionBtn.addEventListener('click', () => setMode('selection'));
233
- contextHeader.addEventListener('click', toggleContextPanel);
234
-
235
- // Provider selection
236
- if (providerSelect) {
237
- providerSelect.addEventListener('change', (e) => {
238
- setProvider(e.target.value);
239
- });
240
- }
241
-
242
- // Model selection
243
- if (modelSelect) {
244
- modelSelect.addEventListener('change', (e) => {
245
- setModel(e.target.value);
246
- });
247
- }
248
-
249
- // Check provider auth status
250
- function checkProviderAuth(provider) {
251
- if (window.electronAPI.checkAuth) {
252
- window.electronAPI.checkAuth(provider);
253
- } else {
254
- // Fallback: use /status command
255
- window.electronAPI.sendMessage('/status');
256
- }
257
- }
258
-
259
- // ===== IPC LISTENERS =====
260
- window.electronAPI.onDotSelected((data) => {
261
- if (data.cancelled) {
262
- addMessage('Selection cancelled', 'system');
263
- setMode('passive');
264
- return;
265
- }
266
-
267
- addMessage(`Selected: ${data.label} at (${data.x}, ${data.y})`, 'system');
268
- addContextItem(data);
269
-
270
- window.electronAPI.getState().then(state => {
271
- currentMode = state.overlayMode;
272
- updateModeDisplay();
273
- });
274
- });
275
-
276
- window.electronAPI.onAgentResponse((data) => {
277
- removeTypingIndicator();
278
- const msgType = data.type === 'error' ? 'system' : 'agent';
279
-
280
- // Check if response contains actions
281
- if (data.hasActions && data.actionData && data.actionData.actions) {
282
- console.log('[CHAT] Received agent response with actions:', data.actionData.actions.length);
283
-
284
- // Show the AI's thought/explanation first (without the JSON)
285
- const cleanText = data.text.replace(/```json[\s\S]*?```/g, '').trim();
286
- if (cleanText) {
287
- addMessage(cleanText, msgType, data.timestamp, {
288
- provider: data.provider,
289
- hasVisualContext: data.hasVisualContext
290
- });
291
- }
292
-
293
- // Show action confirmation UI
294
- showActionConfirmation(data.actionData);
295
- } else {
296
- // Normal response without actions
297
- addMessage(data.text, msgType, data.timestamp, {
298
- provider: data.provider,
299
- hasVisualContext: data.hasVisualContext
300
- });
301
- }
302
- });
303
-
304
- if (window.electronAPI.onAgentTyping) {
305
- window.electronAPI.onAgentTyping((data) => {
306
- if (data.isTyping) {
307
- showTypingIndicator();
308
- } else {
309
- removeTypingIndicator();
310
- }
311
- });
312
- }
313
-
314
- if (window.electronAPI.onScreenCaptured) {
315
- window.electronAPI.onScreenCaptured((data) => {
316
- if (data.error) {
317
- addMessage(`Capture failed: ${data.error}`, 'system');
318
- } else {
319
- addMessage(`Screen captured: ${data.width}x${data.height}. AI can now see your screen.`, 'system', Date.now(), { subtype: 'capture' });
320
- }
321
- });
322
- }
323
-
324
- if (window.electronAPI.onVisualContextUpdate) {
325
- window.electronAPI.onVisualContextUpdate((data) => {
326
- updateVisualContextIndicator(data.count);
327
- });
328
- }
329
-
330
- // Auth status updates
331
- if (window.electronAPI.onAuthStatus) {
332
- window.electronAPI.onAuthStatus((data) => {
333
- updateAuthStatus(data.status, data.provider);
334
- if (data.provider && providerSelect) {
335
- providerSelect.value = data.provider;
336
- currentProvider = data.provider;
337
- }
338
- });
339
- }
340
-
341
- // Token usage updates from API responses
342
- if (window.electronAPI.onTokenUsage) {
343
- window.electronAPI.onTokenUsage((data) => {
344
- if (data.inputTokens) {
345
- totalTokens = data.totalTokens || totalTokens + data.inputTokens + (data.outputTokens || 0);
346
- updateTokenCount();
347
- }
348
- });
349
- }
350
-
351
- // ===== TYPING INDICATOR =====
352
- function showTypingIndicator() {
353
- if (document.getElementById('typing-indicator')) return;
354
-
355
- const typingEl = document.createElement('div');
356
- typingEl.id = 'typing-indicator';
357
- typingEl.className = 'message agent typing';
358
- typingEl.innerHTML = `
359
- <div class="typing-dots">
360
- <span></span><span></span><span></span>
361
- </div>
362
- `;
363
- chatHistory.appendChild(typingEl);
364
- chatHistory.scrollTop = chatHistory.scrollHeight;
365
- }
366
-
367
- function removeTypingIndicator() {
368
- const indicator = document.getElementById('typing-indicator');
369
- if (indicator) indicator.remove();
370
- }
371
-
372
- // ===== VISUAL CONTEXT INDICATOR =====
373
- function updateVisualContextIndicator(count) {
374
- let indicator = document.getElementById('visual-context-indicator');
375
- if (!indicator) {
376
- indicator = document.createElement('div');
377
- indicator.id = 'visual-context-indicator';
378
- indicator.style.cssText = 'position:absolute;top:8px;right:8px;background:var(--accent-green);color:white;padding:2px 8px;border-radius:10px;font-size:10px;';
379
- document.getElementById('toolbar').appendChild(indicator);
380
- }
381
- indicator.textContent = count > 0 ? `📸 ${count}` : '';
382
- indicator.style.display = count > 0 ? 'block' : 'none';
383
- }
384
-
385
- // ===== INITIALIZATION =====
386
- window.electronAPI.getState().then(state => {
387
- currentMode = state.overlayMode;
388
- updateModeDisplay();
389
-
390
- // Load current provider
391
- if (state.aiProvider) {
392
- currentProvider = state.aiProvider;
393
- if (providerSelect) {
394
- providerSelect.value = state.aiProvider;
395
- }
396
- console.log('Current AI provider:', state.aiProvider);
397
- updateModelSelector(state.aiProvider);
398
- }
399
-
400
- // Load current model
401
- if (state.model && modelSelect) {
402
- currentModel = state.model;
403
- modelSelect.value = state.model;
404
- }
405
-
406
- // Check auth status for current provider (async - response comes via onAuthStatus)
407
- checkProviderAuth(currentProvider);
408
- });
409
-
410
- // Initialize auth status display as pending until check completes
411
- updateAuthStatus('pending', currentProvider);
412
- updateModelSelector(currentProvider);
413
-
414
- // ===== AGENTIC ACTION UI =====
415
- function showActionConfirmation(actionData) {
416
- pendingActions = actionData;
417
-
418
- const emptyState = chatHistory.querySelector('.empty-state');
419
- if (emptyState) emptyState.remove();
420
-
421
- const actionEl = document.createElement('div');
422
- actionEl.id = 'action-confirmation';
423
- actionEl.className = 'message agent action-card';
424
-
425
- const actionsHtml = actionData.actions.map((action, idx) => {
426
- let icon = '🖱️';
427
- let desc = '';
428
-
429
- switch (action.type) {
430
- case 'click':
431
- icon = '🖱️';
432
- desc = `Click at (${action.x}, ${action.y})`;
433
- if (action.coordinate) desc = `Click ${action.coordinate}`;
434
- break;
435
- case 'double_click':
436
- icon = '🖱️🖱️';
437
- desc = `Double-click at (${action.x}, ${action.y})`;
438
- break;
439
- case 'right_click':
440
- icon = '🖱️';
441
- desc = `Right-click at (${action.x}, ${action.y})`;
442
- break;
443
- case 'type':
444
- icon = '⌨️';
445
- desc = `Type: "${action.text.substring(0, 30)}${action.text.length > 30 ? '...' : ''}"`;
446
- break;
447
- case 'key':
448
- icon = '⌨️';
449
- desc = `Press: ${action.keys}`;
450
- break;
451
- case 'scroll':
452
- icon = '📜';
453
- desc = `Scroll ${action.direction || 'down'} ${action.amount || 3} lines`;
454
- break;
455
- case 'wait':
456
- icon = '⏳';
457
- desc = `Wait ${action.ms}ms`;
458
- break;
459
- case 'move_mouse':
460
- icon = '➡️';
461
- desc = `Move to (${action.x}, ${action.y})`;
462
- break;
463
- case 'drag':
464
- icon = '✋';
465
- desc = `Drag from (${action.fromX}, ${action.fromY}) to (${action.toX}, ${action.toY})`;
466
- break;
467
- default:
468
- desc = JSON.stringify(action);
469
- }
470
-
471
- return `<div class="action-item"><span class="action-icon">${icon}</span><span class="action-desc">${idx + 1}. ${desc}</span></div>`;
472
- }).join('');
473
-
474
- actionEl.innerHTML = `
475
- <div class="action-header">
476
- <span class="action-title">🤖 AI wants to perform ${actionData.actions.length} action${actionData.actions.length > 1 ? 's' : ''}:</span>
477
- </div>
478
- ${actionData.thought ? `<div class="action-thought">${actionData.thought}</div>` : ''}
479
- <div class="action-list">${actionsHtml}</div>
480
- <div class="action-buttons">
481
- <button id="execute-actions-btn" class="action-btn execute">▶ Execute</button>
482
- <button id="cancel-actions-btn" class="action-btn cancel">✕ Cancel</button>
483
- </div>
484
- `;
485
-
486
- chatHistory.appendChild(actionEl);
487
- chatHistory.scrollTop = chatHistory.scrollHeight;
488
-
489
- // Attach event listeners
490
- document.getElementById('execute-actions-btn').addEventListener('click', executeActions);
491
- document.getElementById('cancel-actions-btn').addEventListener('click', cancelActions);
492
- }
493
-
494
- function executeActions() {
495
- if (!pendingActions) return;
496
-
497
- const confirmEl = document.getElementById('action-confirmation');
498
- if (confirmEl) {
499
- const buttons = confirmEl.querySelector('.action-buttons');
500
- if (buttons) {
501
- buttons.innerHTML = '<span class="executing">⏳ Executing...</span>';
502
- }
503
- }
504
-
505
- window.electronAPI.executeActions(pendingActions);
506
- pendingActions = null;
507
- }
508
-
509
- function cancelActions() {
510
- const confirmEl = document.getElementById('action-confirmation');
511
- if (confirmEl) {
512
- confirmEl.remove();
513
- }
514
-
515
- window.electronAPI.cancelActions();
516
- pendingActions = null;
517
- addMessage('Actions cancelled', 'system');
518
- }
519
-
520
- function showActionProgress(data) {
521
- let progressEl = document.getElementById('action-progress');
522
- if (!progressEl) {
523
- progressEl = document.createElement('div');
524
- progressEl.id = 'action-progress';
525
- progressEl.className = 'message system action-progress';
526
- chatHistory.appendChild(progressEl);
527
- }
528
-
529
- progressEl.textContent = `⏳ ${data.message || `Executing action ${data.current} of ${data.total}...`}`;
530
- chatHistory.scrollTop = chatHistory.scrollHeight;
531
- }
532
-
533
- function showActionComplete(data) {
534
- const confirmEl = document.getElementById('action-confirmation');
535
- if (confirmEl) confirmEl.remove();
536
-
537
- const progressEl = document.getElementById('action-progress');
538
- if (progressEl) progressEl.remove();
539
-
540
- if (data.success) {
541
- addMessage(`✅ ${data.actionsCount} action${data.actionsCount > 1 ? 's' : ''} completed successfully`, 'system');
542
- } else {
543
- addMessage(`❌ Action failed: ${data.error}`, 'system');
544
- }
545
- }
546
-
547
- // Agentic action IPC listeners
548
- if (window.electronAPI.onActionExecuting) {
549
- window.electronAPI.onActionExecuting((data) => {
550
- showActionConfirmation(data);
551
- });
552
- }
553
-
554
- if (window.electronAPI.onActionProgress) {
555
- window.electronAPI.onActionProgress((data) => {
556
- showActionProgress(data);
557
- });
558
- }
559
-
560
- if (window.electronAPI.onActionComplete) {
561
- window.electronAPI.onActionComplete((data) => {
562
- showActionComplete(data);
563
- });
564
- }
565
-
566
- // Add typing indicator styles
567
- const style = document.createElement('style');
568
- style.textContent = `
569
- .message.typing {
570
- padding: 12px 16px;
571
- }
572
- .typing-dots {
573
- display: flex;
574
- gap: 4px;
575
- align-items: center;
576
- }
577
- .typing-dots span {
578
- width: 8px;
579
- height: 8px;
580
- background: var(--text-secondary);
581
- border-radius: 50%;
582
- animation: typing-bounce 1.4s ease-in-out infinite;
583
- }
584
- .typing-dots span:nth-child(2) { animation-delay: 0.2s; }
585
- .typing-dots span:nth-child(3) { animation-delay: 0.4s; }
586
- @keyframes typing-bounce {
587
- 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
588
- 30% { transform: translateY(-8px); opacity: 1; }
589
- }
590
-
591
- /* Action card styles */
592
- .action-card {
593
- background: linear-gradient(135deg, #1a1a2e, #16213e);
594
- border: 1px solid var(--accent-blue);
595
- border-radius: 12px;
596
- padding: 16px;
597
- }
598
- .action-header {
599
- margin-bottom: 8px;
600
- }
601
- .action-title {
602
- font-weight: 600;
603
- color: var(--accent-blue);
604
- }
605
- .action-thought {
606
- font-style: italic;
607
- color: var(--text-secondary);
608
- margin-bottom: 12px;
609
- padding: 8px;
610
- background: rgba(255,255,255,0.05);
611
- border-radius: 6px;
612
- }
613
- .action-list {
614
- margin-bottom: 12px;
615
- }
616
- .action-item {
617
- display: flex;
618
- align-items: center;
619
- gap: 8px;
620
- padding: 6px 8px;
621
- background: rgba(255,255,255,0.05);
622
- border-radius: 4px;
623
- margin-bottom: 4px;
624
- }
625
- .action-icon {
626
- font-size: 16px;
627
- }
628
- .action-desc {
629
- font-family: 'Consolas', monospace;
630
- font-size: 12px;
631
- }
632
- .action-buttons {
633
- display: flex;
634
- gap: 12px;
635
- justify-content: flex-end;
636
- }
637
- .action-btn {
638
- padding: 8px 16px;
639
- border: none;
640
- border-radius: 6px;
641
- cursor: pointer;
642
- font-weight: 600;
643
- font-size: 13px;
644
- transition: all 0.2s;
645
- }
646
- .action-btn.execute {
647
- background: var(--accent-green);
648
- color: white;
649
- }
650
- .action-btn.execute:hover {
651
- background: #00c853;
652
- transform: scale(1.02);
653
- }
654
- .action-btn.cancel {
655
- background: rgba(255,255,255,0.1);
656
- color: var(--text-secondary);
657
- }
658
- .action-btn.cancel:hover {
659
- background: rgba(255,100,100,0.2);
660
- color: #ff6b6b;
661
- }
662
- .executing {
663
- color: var(--accent-blue);
664
- font-style: italic;
665
- }
666
- .action-progress {
667
- background: rgba(0,150,255,0.1);
668
- border-left: 3px solid var(--accent-blue);
669
- }
670
- `;
671
- document.head.appendChild(style);
1
+ // ===== STATE =====
2
+ let currentMode = 'passive';
3
+ let currentProvider = 'copilot';
4
+ let currentModel = 'gpt-4o';
5
+ let totalTokens = 0;
6
+ let messages = [];
7
+ let contextItems = [];
8
+ let pendingActions = null;
9
+
10
+ // ===== CHAT HISTORY PERSISTENCE =====
11
+ const HISTORY_KEY = 'liku-chat-history';
12
+ const MAX_PERSISTED_MESSAGES = 100;
13
+
14
+ function saveHistory() {
15
+ try {
16
+ const toSave = messages.slice(-MAX_PERSISTED_MESSAGES).map(m => ({
17
+ text: m.text,
18
+ type: m.type,
19
+ timestamp: m.timestamp
20
+ }));
21
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(toSave));
22
+ } catch (e) {
23
+ console.warn('[CHAT] Failed to save history:', e);
24
+ }
25
+ }
26
+
27
+ function loadHistory() {
28
+ try {
29
+ const saved = localStorage.getItem(HISTORY_KEY);
30
+ if (saved) {
31
+ const loaded = JSON.parse(saved);
32
+ if (Array.isArray(loaded) && loaded.length > 0) {
33
+ // Remove empty state if present
34
+ const emptyState = chatHistory.querySelector('.empty-state');
35
+ if (emptyState) emptyState.remove();
36
+
37
+ loaded.forEach(msg => {
38
+ // Recreate message elements without re-saving
39
+ const messageEl = document.createElement('div');
40
+ messageEl.className = `message ${msg.type}`;
41
+
42
+ const textEl = document.createElement('div');
43
+ textEl.textContent = msg.text;
44
+ messageEl.appendChild(textEl);
45
+
46
+ const timestampEl = document.createElement('div');
47
+ timestampEl.className = 'timestamp';
48
+ timestampEl.textContent = new Date(msg.timestamp).toLocaleTimeString();
49
+ messageEl.appendChild(timestampEl);
50
+
51
+ chatHistory.appendChild(messageEl);
52
+ messages.push(msg);
53
+ });
54
+
55
+ chatHistory.scrollTop = chatHistory.scrollHeight;
56
+ console.log(`[CHAT] Loaded ${loaded.length} messages from history`);
57
+ }
58
+ }
59
+ } catch (e) {
60
+ console.warn('[CHAT] Failed to load history:', e);
61
+ }
62
+ }
63
+
64
+ function clearHistory() {
65
+ try {
66
+ localStorage.removeItem(HISTORY_KEY);
67
+ messages = [];
68
+ chatHistory.innerHTML = '';
69
+ console.log('[CHAT] History cleared');
70
+ } catch (e) {
71
+ console.warn('[CHAT] Failed to clear history:', e);
72
+ }
73
+ }
74
+
75
+ // ===== ELEMENTS =====
76
+ const chatHistory = document.getElementById('chat-history');
77
+ const messageInput = document.getElementById('message-input');
78
+ const sendButton = document.getElementById('send-button');
79
+ const passiveBtn = document.getElementById('passive-btn');
80
+ const selectionBtn = document.getElementById('selection-btn');
81
+ const minimizeBtn = document.getElementById('minimize-btn');
82
+ const closeBtn = document.getElementById('close-btn');
83
+ const captureBtn = document.getElementById('capture-btn');
84
+ const contextPanel = document.getElementById('context-panel');
85
+ const contextHeader = document.getElementById('context-header');
86
+ const contextContent = document.getElementById('context-content');
87
+ const contextCount = document.getElementById('context-count');
88
+ const providerSelect = document.getElementById('provider-select');
89
+ const modelSelect = document.getElementById('model-select');
90
+ const authStatus = document.getElementById('auth-status');
91
+ const tokenCount = document.getElementById('token-count');
92
+
93
+ function applyElectronAppRegions() {
94
+ const titlebar = document.getElementById('titlebar');
95
+ const titlebarControls = document.getElementById('titlebar-controls');
96
+
97
+ if (titlebar) {
98
+ titlebar.style.setProperty('-webkit-app-region', 'drag');
99
+ }
100
+
101
+ if (titlebarControls) {
102
+ titlebarControls.style.setProperty('-webkit-app-region', 'no-drag');
103
+ }
104
+ }
105
+
106
+ // ===== TOKEN ESTIMATION =====
107
+ // Rough estimate: ~4 chars per token for English text
108
+ function estimateTokens(text) {
109
+ return Math.ceil(text.length / 4);
110
+ }
111
+
112
+ function updateTokenCount(additionalTokens = 0) {
113
+ totalTokens += additionalTokens;
114
+ if (tokenCount) {
115
+ tokenCount.textContent = `${totalTokens.toLocaleString()} tokens`;
116
+ }
117
+ }
118
+
119
+ function resetTokenCount() {
120
+ totalTokens = 0;
121
+ updateTokenCount();
122
+ }
123
+
124
+ // ===== AUTH STATUS =====
125
+ function updateAuthStatus(status, provider) {
126
+ if (!authStatus) return;
127
+
128
+ authStatus.className = 'status-badge';
129
+
130
+ switch (status) {
131
+ case 'connected':
132
+ authStatus.classList.add('connected');
133
+ authStatus.textContent = `${getProviderName(provider)} Connected`;
134
+ break;
135
+ case 'pending':
136
+ authStatus.classList.add('pending');
137
+ authStatus.textContent = 'Authenticating...';
138
+ break;
139
+ case 'error':
140
+ authStatus.classList.add('disconnected');
141
+ authStatus.textContent = 'Auth Error';
142
+ break;
143
+ default:
144
+ authStatus.classList.add('disconnected');
145
+ authStatus.textContent = 'Not Connected';
146
+ }
147
+ }
148
+
149
+ function getProviderName(provider) {
150
+ const names = {
151
+ copilot: 'Copilot',
152
+ openai: 'OpenAI',
153
+ anthropic: 'Anthropic',
154
+ ollama: 'Ollama'
155
+ };
156
+ return names[provider] || provider;
157
+ }
158
+
159
+ // ===== PROVIDER FUNCTIONS =====
160
+ function setProvider(provider) {
161
+ currentProvider = provider;
162
+ if (window.electronAPI.setProvider) {
163
+ window.electronAPI.setProvider(provider);
164
+ }
165
+ // Also send as a command for backward compatibility
166
+ window.electronAPI.sendMessage(`/provider ${provider}`);
167
+ addMessage(`Switched to ${getProviderName(provider)}`, 'system');
168
+
169
+ // Show/hide model selector based on provider
170
+ updateModelSelector(provider);
171
+
172
+ // Check auth status for new provider
173
+ checkProviderAuth(provider);
174
+ }
175
+
176
+ // ===== MODEL FUNCTIONS =====
177
+ function setModel(model) {
178
+ currentModel = model;
179
+ // Send model change command
180
+ window.electronAPI.sendMessage(`/model ${model}`);
181
+ }
182
+
183
+ function updateModelSelector(provider) {
184
+ if (!modelSelect) return;
185
+
186
+ // Only show model selector for Copilot
187
+ modelSelect.style.display = provider === 'copilot' ? 'block' : 'none';
188
+ }
189
+
190
+ // ===== MESSAGE FUNCTIONS =====
191
+ function addMessage(text, type = 'agent', timestamp = Date.now(), extra = {}) {
192
+ const emptyState = chatHistory.querySelector('.empty-state');
193
+ if (emptyState) emptyState.remove();
194
+
195
+ const messageEl = document.createElement('div');
196
+ messageEl.className = `message ${type}`;
197
+ if (extra.subtype) messageEl.classList.add(extra.subtype);
198
+
199
+ const textEl = document.createElement('div');
200
+ textEl.textContent = text;
201
+ messageEl.appendChild(textEl);
202
+
203
+ const timestampEl = document.createElement('div');
204
+ timestampEl.className = 'timestamp';
205
+ timestampEl.textContent = new Date(timestamp).toLocaleTimeString();
206
+ messageEl.appendChild(timestampEl);
207
+
208
+ chatHistory.appendChild(messageEl);
209
+ chatHistory.scrollTop = chatHistory.scrollHeight;
210
+
211
+ messages.push({ text, type, timestamp, ...extra });
212
+
213
+ // Track tokens for user and agent messages
214
+ if (type === 'user' || type === 'agent') {
215
+ updateTokenCount(estimateTokens(text));
216
+ }
217
+
218
+ // Auto-save chat history
219
+ saveHistory();
220
+ }
221
+
222
+ // ===== AGENT ROUTING =====
223
+ // Agent triggers - only for EXPLICIT agent invocations
224
+ // These should be intentional, not accidental matches on common words
225
+ const AGENT_TRIGGERS = {
226
+ research: /\b(research\s+agent|spawn.*research|investigate\s+this|gather\s+info(?:rmation)?)\b/i,
227
+ verify: /\b(verify\s+agent|spawn.*verif|validate\s+this|verification\s+agent)\b/i,
228
+ build: /\b(build\s+agent|spawn.*build|builder\s+agent|code\s+agent)\b/i,
229
+ produce: /(^\\s*\\/produce\\b)|\\b(agentic\\s+producer|producer\\s+agent)\\b/i,
230
+ orchestrate: /\b(spawn\s+(?:a\s+)?(?:sub)?agent|orchestrat|multi-?agent|agent\s+system|coordinate\s+agents?)\b/i
231
+ };
232
+
233
+ function detectAgentIntent(text) {
234
+ // Only trigger on explicit agent invocation phrases
235
+ // Avoid false positives from common words like "check", "build", "create"
236
+ if (AGENT_TRIGGERS.orchestrate.test(text)) return 'orchestrate';
237
+ if (AGENT_TRIGGERS.produce.test(text)) return 'produce';
238
+ if (AGENT_TRIGGERS.research.test(text)) return 'research';
239
+ if (AGENT_TRIGGERS.verify.test(text)) return 'verify';
240
+ if (AGENT_TRIGGERS.build.test(text)) return 'build';
241
+ return null;
242
+ }
243
+
244
+ function extractFirstUrl(text) {
245
+ if (!text || typeof text !== 'string') return null;
246
+ const match = text.match(/https?:\/\/[^\s)]+/i);
247
+ return match ? match[0] : null;
248
+ }
249
+
250
+ function parseProduceOptions(rawText) {
251
+ if (!rawText || typeof rawText !== 'string') {
252
+ return { prompt: rawText || '', options: {} };
253
+ }
254
+
255
+ let prompt = rawText;
256
+ const options = {};
257
+
258
+ if (/--accept-generation\b|--allow-critic-fail\b/i.test(prompt)) {
259
+ options.allowCriticGateFailure = true;
260
+ prompt = prompt
261
+ .replace(/--accept-generation\b/ig, '')
262
+ .replace(/--allow-critic-fail\b/ig, '')
263
+ .trim();
264
+ }
265
+
266
+ return { prompt, options };
267
+ }
268
+
269
+ async function routeToAgent(text, agentType) {
270
+ addMessage(`🤖 Routing to ${agentType} agent...`, 'system');
271
+ showTypingIndicator();
272
+
273
+ try {
274
+ let result;
275
+ switch (agentType) {
276
+ case 'produce': {
277
+ const cleaned = text.replace(/^\\s*\\/produce\\b\\s*/i, '');
278
+ const parsed = parseProduceOptions(cleaned || text);
279
+ const finalPrompt = parsed.prompt || (cleaned || text);
280
+ const referenceUrl = extractFirstUrl(finalPrompt);
281
+ const options = { ...parsed.options };
282
+ if (referenceUrl) {
283
+ options.referenceUrl = referenceUrl;
284
+ }
285
+ result = await window.electronAPI.agentProduce({
286
+ prompt: finalPrompt,
287
+ options
288
+ });
289
+ break;
290
+ }
291
+ case 'research':
292
+ result = await window.electronAPI.agentResearch({ query: text });
293
+ break;
294
+ case 'verify':
295
+ result = await window.electronAPI.agentVerify({ target: text });
296
+ break;
297
+ case 'build':
298
+ result = await window.electronAPI.agentBuild({ specification: text });
299
+ break;
300
+ case 'orchestrate':
301
+ default:
302
+ result = await window.electronAPI.agentRun({ task: text });
303
+ }
304
+
305
+ removeTypingIndicator();
306
+
307
+ if (result.success) {
308
+ const responseText = result.result?.result?.response ||
309
+ result.result?.response ||
310
+ JSON.stringify(result.result, null, 2);
311
+ addMessage(`✅ Agent completed:\n${responseText}`, 'agent');
312
+ } else {
313
+ addMessage(`❌ Agent error: ${result.error}`, 'system');
314
+ // Fallback to regular AI
315
+ addMessage('Falling back to regular AI...', 'system');
316
+ window.electronAPI.sendMessage(text);
317
+ }
318
+ } catch (err) {
319
+ removeTypingIndicator();
320
+ console.error('[CHAT] Agent routing failed:', err);
321
+ addMessage(`⚠️ Agent unavailable: ${err.message}. Using regular AI.`, 'system');
322
+ // Fallback to regular AI
323
+ window.electronAPI.sendMessage(text);
324
+ }
325
+ }
326
+
327
+ function sendMessage() {
328
+ const text = messageInput.value.trim();
329
+ if (!text) return;
330
+
331
+ addMessage(text, 'user');
332
+
333
+ // Check for agent-level tasks
334
+ const agentType = detectAgentIntent(text);
335
+
336
+ if (agentType) {
337
+ routeToAgent(text, agentType);
338
+ } else {
339
+ // Regular AI message
340
+ window.electronAPI.sendMessage(text);
341
+ }
342
+
343
+ messageInput.value = '';
344
+ messageInput.style.height = 'auto';
345
+ }
346
+
347
+ // ===== MODE FUNCTIONS =====
348
+ function updateModeDisplay() {
349
+ passiveBtn.classList.toggle('active', currentMode === 'passive');
350
+ selectionBtn.classList.toggle('active', currentMode === 'selection');
351
+ }
352
+
353
+ function setMode(mode) {
354
+ currentMode = mode;
355
+ window.electronAPI.setMode(mode);
356
+ updateModeDisplay();
357
+
358
+ if (mode === 'selection') {
359
+ addMessage('Selection mode active. Click dots on overlay or scroll to zoom.', 'system');
360
+ } else {
361
+ addMessage('Passive mode. Overlay is click-through.', 'system');
362
+ }
363
+ }
364
+
365
+ // ===== CONTEXT PANEL FUNCTIONS =====
366
+ function addContextItem(data) {
367
+ contextItems.push(data);
368
+ updateContextPanel();
369
+ }
370
+
371
+ function updateContextPanel() {
372
+ contextCount.textContent = contextItems.length;
373
+ contextContent.innerHTML = '';
374
+
375
+ contextItems.forEach((item) => {
376
+ const itemEl = document.createElement('div');
377
+ itemEl.className = 'context-item';
378
+ itemEl.innerHTML = `
379
+ <span class="dot-marker"></span>
380
+ <span>${item.label}</span>
381
+ <span class="coords">(${item.x}, ${item.y})</span>
382
+ `;
383
+ contextContent.appendChild(itemEl);
384
+ });
385
+
386
+ if (contextItems.length > 0) {
387
+ contextPanel.classList.add('expanded');
388
+ }
389
+ }
390
+
391
+ function toggleContextPanel() {
392
+ contextPanel.classList.toggle('expanded');
393
+ }
394
+
395
+ // ===== WINDOW CONTROLS =====
396
+ minimizeBtn.addEventListener('click', () => {
397
+ window.electronAPI.minimizeWindow();
398
+ });
399
+
400
+ closeBtn.addEventListener('click', () => {
401
+ window.electronAPI.hideWindow();
402
+ });
403
+
404
+ // ===== CAPTURE FUNCTION =====
405
+ captureBtn.addEventListener('click', () => {
406
+ addMessage('Initiating screen capture...', 'system', Date.now(), { subtype: 'capture' });
407
+ window.electronAPI.captureScreen();
408
+ });
409
+
410
+ // ===== EVENT LISTENERS =====
411
+ sendButton.addEventListener('click', sendMessage);
412
+
413
+ messageInput.addEventListener('keydown', (e) => {
414
+ if (e.key === 'Enter' && !e.shiftKey) {
415
+ e.preventDefault();
416
+ sendMessage();
417
+ }
418
+ });
419
+
420
+ // Auto-resize textarea
421
+ messageInput.addEventListener('input', () => {
422
+ messageInput.style.height = 'auto';
423
+ messageInput.style.height = Math.min(messageInput.scrollHeight, 120) + 'px';
424
+ });
425
+
426
+ passiveBtn.addEventListener('click', () => setMode('passive'));
427
+ selectionBtn.addEventListener('click', () => setMode('selection'));
428
+ contextHeader.addEventListener('click', toggleContextPanel);
429
+
430
+ // Provider selection
431
+ if (providerSelect) {
432
+ providerSelect.addEventListener('change', (e) => {
433
+ setProvider(e.target.value);
434
+ });
435
+ }
436
+
437
+ // Model selection
438
+ if (modelSelect) {
439
+ modelSelect.addEventListener('change', (e) => {
440
+ setModel(e.target.value);
441
+ });
442
+ }
443
+
444
+ // Check provider auth status
445
+ function checkProviderAuth(provider) {
446
+ if (window.electronAPI.checkAuth) {
447
+ window.electronAPI.checkAuth(provider);
448
+ } else {
449
+ // Fallback: use /status command
450
+ window.electronAPI.sendMessage('/status');
451
+ }
452
+ }
453
+
454
+ // ===== IPC LISTENERS =====
455
+ window.electronAPI.onDotSelected((data) => {
456
+ if (data.cancelled) {
457
+ addMessage('Selection cancelled', 'system');
458
+ setMode('passive');
459
+ return;
460
+ }
461
+
462
+ addMessage(`Selected: ${data.label} at (${data.x}, ${data.y})`, 'system');
463
+ addContextItem(data);
464
+
465
+ window.electronAPI.getState().then(state => {
466
+ currentMode = state.overlayMode;
467
+ updateModeDisplay();
468
+ });
469
+ });
470
+
471
+ window.electronAPI.onAgentResponse((data) => {
472
+ removeTypingIndicator();
473
+ const msgType = data.type === 'error' ? 'system' : 'agent';
474
+
475
+ // Check if response contains actions
476
+ if (data.hasActions && data.actionData && data.actionData.actions) {
477
+ console.log('[CHAT] Received agent response with actions:', data.actionData.actions.length);
478
+
479
+ // Show the AI's thought/explanation first (without the JSON)
480
+ const cleanText = data.text.replace(/```json[\s\S]*?```/g, '').trim();
481
+ if (cleanText) {
482
+ addMessage(cleanText, msgType, data.timestamp, {
483
+ provider: data.provider,
484
+ hasVisualContext: data.hasVisualContext
485
+ });
486
+ }
487
+
488
+ // Show action confirmation UI
489
+ showActionConfirmation(data.actionData);
490
+ } else {
491
+ // Normal response without actions
492
+ addMessage(data.text, msgType, data.timestamp, {
493
+ provider: data.provider,
494
+ hasVisualContext: data.hasVisualContext
495
+ });
496
+ }
497
+ });
498
+
499
+ if (window.electronAPI.onAgentTyping) {
500
+ window.electronAPI.onAgentTyping((data) => {
501
+ if (data.isTyping) {
502
+ showTypingIndicator();
503
+ } else {
504
+ removeTypingIndicator();
505
+ }
506
+ });
507
+ }
508
+
509
+ if (window.electronAPI.onScreenCaptured) {
510
+ window.electronAPI.onScreenCaptured((data) => {
511
+ if (data.error) {
512
+ addMessage(`Capture failed: ${data.error}`, 'system');
513
+ } else {
514
+ addMessage(`Screen captured: ${data.width}x${data.height}. AI can now see your screen.`, 'system', Date.now(), { subtype: 'capture' });
515
+ }
516
+ });
517
+ }
518
+
519
+ if (window.electronAPI.onVisualContextUpdate) {
520
+ window.electronAPI.onVisualContextUpdate((data) => {
521
+ updateVisualContextIndicator(data.count);
522
+ });
523
+ }
524
+
525
+ // Auth status updates
526
+ if (window.electronAPI.onAuthStatus) {
527
+ window.electronAPI.onAuthStatus((data) => {
528
+ updateAuthStatus(data.status, data.provider);
529
+ if (data.provider && providerSelect) {
530
+ providerSelect.value = data.provider;
531
+ currentProvider = data.provider;
532
+ }
533
+ });
534
+ }
535
+
536
+ // Token usage updates from API responses
537
+ if (window.electronAPI.onTokenUsage) {
538
+ window.electronAPI.onTokenUsage((data) => {
539
+ if (data.inputTokens) {
540
+ totalTokens = data.totalTokens || totalTokens + data.inputTokens + (data.outputTokens || 0);
541
+ updateTokenCount();
542
+ }
543
+ });
544
+ }
545
+
546
+ // ===== TYPING INDICATOR =====
547
+ function showTypingIndicator() {
548
+ if (document.getElementById('typing-indicator')) return;
549
+
550
+ const typingEl = document.createElement('div');
551
+ typingEl.id = 'typing-indicator';
552
+ typingEl.className = 'message agent typing';
553
+ typingEl.innerHTML = `
554
+ <div class="typing-dots">
555
+ <span></span><span></span><span></span>
556
+ </div>
557
+ `;
558
+ chatHistory.appendChild(typingEl);
559
+ chatHistory.scrollTop = chatHistory.scrollHeight;
560
+ }
561
+
562
+ function removeTypingIndicator() {
563
+ const indicator = document.getElementById('typing-indicator');
564
+ if (indicator) indicator.remove();
565
+ }
566
+
567
+ // ===== VISUAL CONTEXT INDICATOR =====
568
+ function updateVisualContextIndicator(count) {
569
+ let indicator = document.getElementById('visual-context-indicator');
570
+ if (!indicator) {
571
+ indicator = document.createElement('div');
572
+ indicator.id = 'visual-context-indicator';
573
+ indicator.style.cssText = 'position:absolute;top:8px;right:8px;background:var(--accent-green);color:white;padding:2px 8px;border-radius:10px;font-size:10px;';
574
+ document.getElementById('toolbar').appendChild(indicator);
575
+ }
576
+ indicator.textContent = count > 0 ? `📸 ${count}` : '';
577
+ indicator.style.display = count > 0 ? 'block' : 'none';
578
+ }
579
+
580
+ // ===== INITIALIZATION =====
581
+ // Load persisted chat history first
582
+ loadHistory();
583
+ applyElectronAppRegions();
584
+
585
+ window.electronAPI.getState().then(state => {
586
+ currentMode = state.overlayMode;
587
+ updateModeDisplay();
588
+
589
+ // Load current provider
590
+ if (state.aiProvider) {
591
+ currentProvider = state.aiProvider;
592
+ if (providerSelect) {
593
+ providerSelect.value = state.aiProvider;
594
+ }
595
+ console.log('Current AI provider:', state.aiProvider);
596
+ updateModelSelector(state.aiProvider);
597
+ }
598
+
599
+ // Load current model
600
+ if (state.model && modelSelect) {
601
+ currentModel = state.model;
602
+ modelSelect.value = state.model;
603
+ }
604
+
605
+ // Check auth status for current provider (async - response comes via onAuthStatus)
606
+ checkProviderAuth(currentProvider);
607
+ });
608
+
609
+ // Initialize auth status display as pending until check completes
610
+ updateAuthStatus('pending', currentProvider);
611
+ updateModelSelector(currentProvider);
612
+
613
+ // ===== AGENTIC ACTION UI =====
614
+ function showActionConfirmation(actionData) {
615
+ pendingActions = actionData;
616
+
617
+ const emptyState = chatHistory.querySelector('.empty-state');
618
+ if (emptyState) emptyState.remove();
619
+
620
+ const actionEl = document.createElement('div');
621
+ actionEl.id = 'action-confirmation';
622
+ actionEl.className = 'message agent action-card';
623
+
624
+ const actionsHtml = actionData.actions.map((action, idx) => {
625
+ let icon = '🖱️';
626
+ let desc = '';
627
+
628
+ switch (action.type) {
629
+ case 'click':
630
+ icon = '🖱️';
631
+ desc = `Click at (${action.x}, ${action.y})`;
632
+ if (action.coordinate) desc = `Click ${action.coordinate}`;
633
+ break;
634
+ case 'double_click':
635
+ icon = '🖱️🖱️';
636
+ desc = `Double-click at (${action.x}, ${action.y})`;
637
+ break;
638
+ case 'right_click':
639
+ icon = '🖱️';
640
+ desc = `Right-click at (${action.x}, ${action.y})`;
641
+ break;
642
+ case 'type':
643
+ icon = '⌨️';
644
+ desc = `Type: "${action.text.substring(0, 30)}${action.text.length > 30 ? '...' : ''}"`;
645
+ break;
646
+ case 'key':
647
+ icon = '⌨️';
648
+ desc = `Press: ${action.key || action.keys || 'unknown'}`;
649
+ break;
650
+ case 'scroll':
651
+ icon = '📜';
652
+ desc = `Scroll ${action.direction || 'down'} ${action.amount || 3} lines`;
653
+ break;
654
+ case 'wait':
655
+ icon = '⏳';
656
+ desc = `Wait ${action.ms}ms`;
657
+ break;
658
+ case 'move_mouse':
659
+ icon = '➡️';
660
+ desc = `Move to (${action.x}, ${action.y})`;
661
+ break;
662
+ case 'drag':
663
+ icon = '✋';
664
+ desc = `Drag from (${action.fromX}, ${action.fromY}) to (${action.toX}, ${action.toY})`;
665
+ break;
666
+ case 'run_command':
667
+ icon = '💻';
668
+ const cmdDisplay = action.command.length > 40
669
+ ? action.command.substring(0, 40) + '...'
670
+ : action.command;
671
+ desc = `Run: <code>${cmdDisplay}</code>`;
672
+ if (action.cwd) desc += ` in ${action.cwd}`;
673
+ break;
674
+ default:
675
+ desc = JSON.stringify(action);
676
+ }
677
+
678
+ return `<div class="action-item"><span class="action-icon">${icon}</span><span class="action-desc">${idx + 1}. ${desc}</span></div>`;
679
+ }).join('');
680
+
681
+ actionEl.innerHTML = `
682
+ <div class="action-header">
683
+ <span class="action-title">🤖 AI wants to perform ${actionData.actions.length} action${actionData.actions.length > 1 ? 's' : ''}:</span>
684
+ </div>
685
+ ${actionData.thought ? `<div class="action-thought">${actionData.thought}</div>` : ''}
686
+ <div class="action-list">${actionsHtml}</div>
687
+ <div class="action-buttons">
688
+ <button id="execute-actions-btn" class="action-btn execute">▶ Execute</button>
689
+ <button id="cancel-actions-btn" class="action-btn cancel">✕ Cancel</button>
690
+ </div>
691
+ `;
692
+
693
+ chatHistory.appendChild(actionEl);
694
+ chatHistory.scrollTop = chatHistory.scrollHeight;
695
+
696
+ // Attach event listeners
697
+ document.getElementById('execute-actions-btn').addEventListener('click', executeActions);
698
+ document.getElementById('cancel-actions-btn').addEventListener('click', cancelActions);
699
+ }
700
+
701
+ function executeActions() {
702
+ if (!pendingActions) return;
703
+
704
+ const confirmEl = document.getElementById('action-confirmation');
705
+ if (confirmEl) {
706
+ const buttons = confirmEl.querySelector('.action-buttons');
707
+ if (buttons) {
708
+ buttons.innerHTML = '<span class="executing">⏳ Executing...</span>';
709
+ }
710
+ }
711
+
712
+ window.electronAPI.executeActions(pendingActions);
713
+ pendingActions = null;
714
+ }
715
+
716
+ function cancelActions() {
717
+ const confirmEl = document.getElementById('action-confirmation');
718
+ if (confirmEl) {
719
+ confirmEl.remove();
720
+ }
721
+
722
+ window.electronAPI.cancelActions();
723
+ pendingActions = null;
724
+ addMessage('Actions cancelled', 'system');
725
+ }
726
+
727
+ function showActionProgress(data) {
728
+ let progressEl = document.getElementById('action-progress');
729
+ if (!progressEl) {
730
+ progressEl = document.createElement('div');
731
+ progressEl.id = 'action-progress';
732
+ progressEl.className = 'message system action-progress';
733
+ chatHistory.appendChild(progressEl);
734
+ }
735
+
736
+ progressEl.textContent = `⏳ ${data.message || `Executing action ${data.current} of ${data.total}...`}`;
737
+ chatHistory.scrollTop = chatHistory.scrollHeight;
738
+ }
739
+
740
+ function showActionComplete(data) {
741
+ const confirmEl = document.getElementById('action-confirmation');
742
+ if (confirmEl) confirmEl.remove();
743
+
744
+ const progressEl = document.getElementById('action-progress');
745
+ if (progressEl) progressEl.remove();
746
+
747
+ if (data.success) {
748
+ addMessage(`✅ ${data.actionsCount} action${data.actionsCount > 1 ? 's' : ''} completed successfully`, 'system');
749
+ } else {
750
+ addMessage(`❌ Action failed: ${data.error}`, 'system');
751
+ }
752
+ }
753
+
754
+ // Agentic action IPC listeners
755
+ if (window.electronAPI.onActionExecuting) {
756
+ window.electronAPI.onActionExecuting((data) => {
757
+ showActionConfirmation(data);
758
+ });
759
+ }
760
+
761
+ if (window.electronAPI.onActionProgress) {
762
+ window.electronAPI.onActionProgress((data) => {
763
+ showActionProgress(data);
764
+ });
765
+ }
766
+
767
+ if (window.electronAPI.onActionComplete) {
768
+ window.electronAPI.onActionComplete((data) => {
769
+ showActionComplete(data);
770
+ });
771
+ }
772
+
773
+ // ===== AGENT EVENT HANDLING =====
774
+ if (window.electronAPI.onAgentEvent) {
775
+ window.electronAPI.onAgentEvent((data) => {
776
+ console.log('[CHAT] Agent event:', data.type, data);
777
+ switch (data.type) {
778
+ case 'session-started':
779
+ addMessage(`🚀 Agent session started: ${data.sessionId}`, 'system');
780
+ break;
781
+ case 'execution-started':
782
+ showTypingIndicator();
783
+ addMessage(`⚙️ Agent working on: ${typeof data.task === 'string' ? data.task : data.task?.description || 'task'}`, 'system');
784
+ break;
785
+ case 'execution-complete':
786
+ removeTypingIndicator();
787
+ if (data.result?.success) {
788
+ const response = data.result.result?.response ||
789
+ data.result.response ||
790
+ 'Task completed successfully';
791
+ addMessage(`✅ ${response}`, 'agent');
792
+ } else {
793
+ addMessage(`❌ Agent failed: ${data.result?.error || 'Unknown error'}`, 'system');
794
+ }
795
+ break;
796
+ case 'execution-error':
797
+ removeTypingIndicator();
798
+ addMessage(`❌ Agent error: ${data.error}`, 'system');
799
+ break;
800
+ case 'handoff':
801
+ addMessage(`🔄 Agent handoff: ${data.from} → ${data.to}`, 'system');
802
+ break;
803
+ case 'agent:log':
804
+ console.log('[AGENT LOG]', data);
805
+ break;
806
+ }
807
+ });
808
+ }
809
+
810
+ // Add typing indicator styles
811
+ const style = document.createElement('style');
812
+ style.textContent = `
813
+ .message.typing {
814
+ padding: 12px 16px;
815
+ }
816
+ .typing-dots {
817
+ display: flex;
818
+ gap: 4px;
819
+ align-items: center;
820
+ }
821
+ .typing-dots span {
822
+ width: 8px;
823
+ height: 8px;
824
+ background: var(--text-secondary);
825
+ border-radius: 50%;
826
+ animation: typing-bounce 1.4s ease-in-out infinite;
827
+ }
828
+ .typing-dots span:nth-child(2) { animation-delay: 0.2s; }
829
+ .typing-dots span:nth-child(3) { animation-delay: 0.4s; }
830
+ @keyframes typing-bounce {
831
+ 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
832
+ 30% { transform: translateY(-8px); opacity: 1; }
833
+ }
834
+
835
+ /* Action card styles */
836
+ .action-card {
837
+ background: linear-gradient(135deg, #1a1a2e, #16213e);
838
+ border: 1px solid var(--accent-blue);
839
+ border-radius: 12px;
840
+ padding: 16px;
841
+ }
842
+ .action-header {
843
+ margin-bottom: 8px;
844
+ }
845
+ .action-title {
846
+ font-weight: 600;
847
+ color: var(--accent-blue);
848
+ }
849
+ .action-thought {
850
+ font-style: italic;
851
+ color: var(--text-secondary);
852
+ margin-bottom: 12px;
853
+ padding: 8px;
854
+ background: rgba(255,255,255,0.05);
855
+ border-radius: 6px;
856
+ }
857
+ .action-list {
858
+ margin-bottom: 12px;
859
+ }
860
+ .action-item {
861
+ display: flex;
862
+ align-items: center;
863
+ gap: 8px;
864
+ padding: 6px 8px;
865
+ background: rgba(255,255,255,0.05);
866
+ border-radius: 4px;
867
+ margin-bottom: 4px;
868
+ }
869
+ .action-icon {
870
+ font-size: 16px;
871
+ }
872
+ .action-desc {
873
+ font-family: 'Consolas', monospace;
874
+ font-size: 12px;
875
+ }
876
+ .action-buttons {
877
+ display: flex;
878
+ gap: 12px;
879
+ justify-content: flex-end;
880
+ }
881
+ .action-btn {
882
+ padding: 8px 16px;
883
+ border: none;
884
+ border-radius: 6px;
885
+ cursor: pointer;
886
+ font-weight: 600;
887
+ font-size: 13px;
888
+ transition: all 0.2s;
889
+ }
890
+ .action-btn.execute {
891
+ background: var(--accent-green);
892
+ color: white;
893
+ }
894
+ .action-btn.execute:hover {
895
+ background: #00c853;
896
+ transform: scale(1.02);
897
+ }
898
+ .action-btn.cancel {
899
+ background: rgba(255,255,255,0.1);
900
+ color: var(--text-secondary);
901
+ }
902
+ .action-btn.cancel:hover {
903
+ background: rgba(255,100,100,0.2);
904
+ color: #ff6b6b;
905
+ }
906
+ .executing {
907
+ color: var(--accent-blue);
908
+ font-style: italic;
909
+ }
910
+ .action-progress {
911
+ background: rgba(0,150,255,0.1);
912
+ border-left: 3px solid var(--accent-blue);
913
+ }
914
+ `;
915
+ document.head.appendChild(style);