vite-plugin-opencode-assistant 1.0.2 → 1.0.3

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,2288 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview OpenCode 挂件客户端脚本
4
+ * @description 用于在浏览器中显示 OpenCode AI 助手挂件
5
+ */
6
+ (function () {
7
+ 'use strict';
8
+ /** @type {string} 初始化标记 */
9
+ const INIT_MARKER = '__OPENCODE_INITIALIZED__';
10
+ /** @type {string} 选中元素存储键 */
11
+ const SELECTED_ELEMENTS_KEY = '__opencode_selected_elements__';
12
+ /** @type {number} 服务器同步间隔(毫秒) */
13
+ const SERVER_SYNC_INTERVAL = 2000;
14
+ /** @type {number} 检查 Vue Inspector 间隔(毫秒) */
15
+ const INSPECTOR_CHECK_INTERVAL = 500;
16
+ /** @type {number} 自动打开延迟(毫秒) */
17
+ const AUTO_OPEN_DELAY = 1000;
18
+ /** @type {number} 通知显示时间(毫秒) */
19
+ const NOTIFICATION_DURATION = 3000;
20
+ /**
21
+ * @typedef {Object} HotkeyConfig
22
+ * @property {boolean} ctrl - 是否需要 Ctrl/Meta 键
23
+ * @property {boolean} shift - 是否需要 Shift 键
24
+ * @property {boolean} alt - 是否需要 Alt 键
25
+ * @property {string} key - 主键
26
+ */
27
+ /**
28
+ * @typedef {Object} SelectedElement
29
+ * @property {string|null} filePath - 文件路径
30
+ * @property {number|null} line - 行号
31
+ * @property {number|null} column - 列号
32
+ * @property {string} innerText - 元素内部文本
33
+ * @property {string} description - 元素描述(标签名+选择器)
34
+ */
35
+ /**
36
+ * @typedef {Object} WidgetConfig
37
+ * @property {string} webUrl - Web 服务 URL
38
+ * @property {string} position - 挂件位置
39
+ * @property {string} theme - 主题模式
40
+ * @property {boolean} open - 是否自动打开
41
+ * @property {string} sessionUrl - 会话 URL
42
+ * @property {boolean} lazy - 是否懒加载
43
+ * @property {string} hotkey - 快捷键配置
44
+ */
45
+ /**
46
+ * 初始化 OpenCode 挂件
47
+ * @param {WidgetConfig} config - 挂件配置
48
+ */
49
+ function initOpenCodeWidget(config) {
50
+ if (window[INIT_MARKER])
51
+ return;
52
+ window[INIT_MARKER] = true;
53
+ const { webUrl, position, theme, open, sessionUrl, lazy, hotkey, cwd } = config;
54
+ /** @type {string} 当前页面 URL */
55
+ let currentPageUrl = '';
56
+ /** @type {string} 当前页面标题 */
57
+ let currentPageTitle = '';
58
+ /** @type {boolean} 服务是否已启动 */
59
+ let servicesStarted = !lazy;
60
+ /** @type {boolean} 挂件是否打开 */
61
+ let isOpen = false;
62
+ /**
63
+ * 解析快捷键字符串
64
+ * @param {string} hotkeyStr - 快捷键字符串,如 'ctrl+k'
65
+ * @returns {HotkeyConfig} 快捷键配置
66
+ */
67
+ function parseHotkey(hotkeyStr) {
68
+ if (!hotkeyStr)
69
+ return { ctrl: true, shift: false, alt: false, key: 'k' };
70
+ const parts = hotkeyStr.toLowerCase().split('+');
71
+ const key = parts.pop();
72
+ return {
73
+ ctrl: parts.includes('ctrl') || parts.includes('cmd') || parts.includes('meta'),
74
+ shift: parts.includes('shift'),
75
+ alt: parts.includes('alt'),
76
+ key: key || 'k'
77
+ };
78
+ }
79
+ /** @type {HotkeyConfig} 主快捷键配置 */
80
+ const mainHotkey = parseHotkey(hotkey);
81
+ /** @type {HotkeyConfig} 选择模式快捷键配置 */
82
+ const selectHotkey = parseHotkey('ctrl+p');
83
+ /**
84
+ * 检查键盘事件是否匹配快捷键
85
+ * @param {KeyboardEvent} e - 键盘事件
86
+ * @param {HotkeyConfig} hotkeyConfig - 快捷键配置
87
+ * @returns {boolean} 是否匹配
88
+ */
89
+ function matchHotkey(e, hotkeyConfig) {
90
+ const ctrlMatch = hotkeyConfig.ctrl ? (e.ctrlKey || e.metaKey) : !(e.ctrlKey || e.metaKey);
91
+ const shiftMatch = hotkeyConfig.shift ? e.shiftKey : !e.shiftKey;
92
+ const altMatch = hotkeyConfig.alt ? e.altKey : !e.altKey;
93
+ const keyMatch = e.key.toLowerCase() === hotkeyConfig.key.toLowerCase();
94
+ return ctrlMatch && shiftMatch && altMatch && keyMatch;
95
+ }
96
+ /**
97
+ * 从 sessionStorage 加载选中的元素
98
+ * @returns {SelectedElement[]} 选中的元素列表
99
+ */
100
+ function loadSelectedElements() {
101
+ try {
102
+ const stored = sessionStorage.getItem(SELECTED_ELEMENTS_KEY);
103
+ if (stored) {
104
+ return JSON.parse(stored);
105
+ }
106
+ }
107
+ catch (e) {
108
+ // 忽略错误
109
+ }
110
+ return [];
111
+ }
112
+ /**
113
+ * 保存选中的元素到 sessionStorage
114
+ * @param {SelectedElement[]} elements - 选中的元素列表
115
+ */
116
+ function saveSelectedElements(elements) {
117
+ try {
118
+ sessionStorage.setItem(SELECTED_ELEMENTS_KEY, JSON.stringify(elements));
119
+ }
120
+ catch (e) {
121
+ // 忽略错误
122
+ }
123
+ }
124
+ /** @type {SelectedElement[]} 选中的元素列表 */
125
+ let selectedElements = loadSelectedElements();
126
+ /**
127
+ * 确保服务已启动
128
+ * @returns {Promise<boolean>} 是否成功启动
129
+ */
130
+ async function ensureServicesStarted() {
131
+ if (servicesStarted)
132
+ return true;
133
+ try {
134
+ const res = await fetch('/__opencode_start__');
135
+ const data = await res.json();
136
+ if (data.success) {
137
+ servicesStarted = true;
138
+ if (data.sessionUrl && iframe) {
139
+ iframe.src = data.sessionUrl;
140
+ }
141
+ return true;
142
+ }
143
+ }
144
+ catch (e) {
145
+ console.error('[OpenCode Widget] Failed to start services:', e);
146
+ }
147
+ return false;
148
+ }
149
+ /**
150
+ * 更新页面上下文
151
+ * @param {boolean} [force=false] - 是否强制更新
152
+ */
153
+ function updateContext(force = false) {
154
+ if (!servicesStarted)
155
+ return;
156
+ const newUrl = window.location.href;
157
+ const newTitle = document.title;
158
+ if (force || newUrl !== currentPageUrl || newTitle !== currentPageTitle) {
159
+ currentPageUrl = newUrl;
160
+ currentPageTitle = newTitle;
161
+ fetch('/__opencode_context__', {
162
+ method: 'POST',
163
+ headers: { 'Content-Type': 'application/json' },
164
+ body: JSON.stringify({ url: newUrl, title: newTitle, selectedElements })
165
+ }).catch(() => { });
166
+ }
167
+ }
168
+ // 监听路由变化
169
+ const originalPushState = history.pushState;
170
+ const originalReplaceState = history.replaceState;
171
+ history.pushState = function (...args) {
172
+ originalPushState.apply(this, args);
173
+ setTimeout(updateContext, 0);
174
+ };
175
+ history.replaceState = function (...args) {
176
+ originalReplaceState.apply(this, args);
177
+ setTimeout(updateContext, 0);
178
+ };
179
+ window.addEventListener('popstate', () => setTimeout(updateContext, 0));
180
+ window.addEventListener('hashchange', () => setTimeout(updateContext, 0));
181
+ // 监听标题变化
182
+ const titleObserver = new MutationObserver(() => {
183
+ if (document.title !== currentPageTitle) {
184
+ updateContext();
185
+ }
186
+ });
187
+ if (document.head) {
188
+ titleObserver.observe(document.head, { childList: true, subtree: true });
189
+ }
190
+ if (servicesStarted) {
191
+ updateContext(true);
192
+ }
193
+ /** @type {string} iframe URL */
194
+ const iframeUrl = sessionUrl || webUrl;
195
+ // 创建样式
196
+ const style = document.createElement('style');
197
+ style.textContent = buildWidgetStyles();
198
+ document.head.appendChild(style);
199
+ // 创建容器
200
+ const container = document.createElement('div');
201
+ container.className = `opencode-widget ${position}`;
202
+ // 创建按钮
203
+ const button = document.createElement('button');
204
+ button.className = 'opencode-button';
205
+ button.innerHTML = `
206
+ <svg t="1775402599580" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns=" http://www.w3.org/2000/svg " p-id="5390" xmlns:xlink=" http://www.w3.org/1999/xlink " width="100%" height="100%"><path d="M512 981.33H85.34c-15.85 0-30.38-8.77-37.77-22.81a42.624 42.624 0 0 1 2.6-44.02L135 791.08C75.25 710.5 42.67 612.6 42.67 512 42.67 253.21 253.21 42.67 512 42.67S981.34 253.21 981.34 512 770.8 981.33 512 981.33zM166.44 896H512c211.73 0 384-172.27 384-384S723.73 128 512 128 128 300.27 128 512c0 91.29 32.83 179.9 92.46 249.46 12.58 14.69 13.73 36 2.77 51.94L166.44 896z" fill="white" p-id="5391"></path><path d="M384 448m-64 0a64 64 0 1 0 128 0 64 64 0 1 0 -128 0Z" fill="white" p-id="5392"></path><path d="M640 448m-64 0a64 64 0 1 0 128 0 64 64 0 1 0 -128 0Z" fill="white" p-id="5393"></path></svg>
207
+ `;
208
+ button.setAttribute('aria-label', '打开 AI 助手');
209
+ button.title = `AI 助手 (${hotkey || 'Ctrl+K'})`;
210
+ // 创建聊天面板
211
+ const chat = document.createElement('div');
212
+ chat.className = 'opencode-chat';
213
+ chat.setAttribute('role', 'dialog');
214
+ chat.setAttribute('aria-modal', 'true');
215
+ chat.setAttribute('aria-label', 'AI 助手对话窗口');
216
+ // 创建面板头部操作栏
217
+ const chatHeader = document.createElement('div');
218
+ chatHeader.className = 'opencode-chat-header';
219
+ // 左侧操作区
220
+ const headerLeft = document.createElement('div');
221
+ headerLeft.className = 'opencode-chat-header-left';
222
+ // 会话列表折叠按钮
223
+ const toggleBtn = document.createElement('button');
224
+ toggleBtn.className = 'opencode-header-btn session-toggle';
225
+ toggleBtn.innerHTML = `
226
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
227
+ <path d="M4 6h16M4 12h16M4 18h16" stroke-linecap="round"/>
228
+ </svg>
229
+ `;
230
+ toggleBtn.title = '展开会话列表';
231
+ toggleBtn.setAttribute('aria-label', '展开会话列表');
232
+ toggleBtn.setAttribute('aria-expanded', 'false');
233
+ // 选择元素按钮
234
+ const selectButton = document.createElement('button');
235
+ selectButton.className = 'opencode-header-btn select-btn';
236
+ selectButton.innerHTML = `
237
+ <svg viewBox="0 0 1024 1024" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
238
+ <path fill="currentColor" d="M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"></path><path fill="currentColor" d="M512 96a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V128a32 32 0 0 1 32-32m0 576a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V704a32 32 0 0 1 32-32M96 512a32 32 0 0 1 32-32h192a32 32 0 0 1 0 64H128a32 32 0 0 1-32-32m576 0a32 32 0 0 1 32-32h192a32 32 0 1 1 0 64H704a32 32 0 0 1-32-32"></path>
239
+ </svg>
240
+ `;
241
+ selectButton.title = '选择页面元素 (Ctrl+P)';
242
+ selectButton.setAttribute('aria-label', '选择页面元素');
243
+ selectButton.setAttribute('aria-pressed', 'false');
244
+ headerLeft.appendChild(toggleBtn);
245
+ headerLeft.appendChild(selectButton);
246
+ // 标题
247
+ const headerTitle = document.createElement('span');
248
+ headerTitle.className = 'opencode-chat-header-title';
249
+ headerTitle.textContent = 'AI 助手';
250
+ // 右侧操作区
251
+ const headerActions = document.createElement('div');
252
+ headerActions.className = 'opencode-chat-header-actions';
253
+ // 关闭按钮
254
+ const closeBtn = document.createElement('button');
255
+ closeBtn.className = 'opencode-header-btn close';
256
+ closeBtn.innerHTML = `
257
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
258
+ <path d="M18 6L6 18M6 6l12 12"/>
259
+ </svg>
260
+ `;
261
+ closeBtn.title = '关闭';
262
+ closeBtn.setAttribute('aria-label', '关闭面板');
263
+ closeBtn.addEventListener('click', () => {
264
+ isOpen = false;
265
+ chat.classList.remove('open');
266
+ button.classList.remove('active');
267
+ });
268
+ headerActions.appendChild(closeBtn);
269
+ chatHeader.appendChild(headerLeft);
270
+ chatHeader.appendChild(headerTitle);
271
+ chatHeader.appendChild(headerActions);
272
+ // 创建会话列表
273
+ const sessionList = document.createElement('div');
274
+ sessionList.className = 'opencode-session-list collapsed';
275
+ // 创建会话列表头部
276
+ const sessionListHeader = document.createElement('div');
277
+ sessionListHeader.className = 'opencode-session-list-header';
278
+ sessionListHeader.innerHTML = `
279
+ <span id="opencode-session-list-title">会话列表</span>
280
+ <button class="opencode-new-session-btn" title="新建会话" aria-label="新建会话">+</button>
281
+ `;
282
+ // 创建会话列表头部骨架屏
283
+ const sessionHeaderSkeleton = document.createElement('div');
284
+ sessionHeaderSkeleton.className = 'opencode-session-header-skeleton';
285
+ sessionHeaderSkeleton.innerHTML = `
286
+ <div class="opencode-skeleton-header-title"></div>
287
+ <div class="opencode-skeleton-header-btn"></div>
288
+ `;
289
+ // 创建会话列表内容
290
+ const sessionListContent = document.createElement('div');
291
+ sessionListContent.className = 'opencode-session-list-content';
292
+ sessionListContent.setAttribute('role', 'listbox');
293
+ sessionListContent.setAttribute('aria-labelledby', 'opencode-session-list-title');
294
+ // 创建会话列表内容骨架屏
295
+ const sessionSkeleton = document.createElement('div');
296
+ sessionSkeleton.className = 'opencode-session-skeleton';
297
+ sessionSkeleton.innerHTML = `
298
+ <div class="opencode-skeleton-item">
299
+ <div class="opencode-skeleton-title"></div>
300
+ <div class="opencode-skeleton-meta"></div>
301
+ </div>
302
+ <div class="opencode-skeleton-item">
303
+ <div class="opencode-skeleton-title"></div>
304
+ <div class="opencode-skeleton-meta"></div>
305
+ </div>
306
+ <div class="opencode-skeleton-item">
307
+ <div class="opencode-skeleton-title"></div>
308
+ <div class="opencode-skeleton-meta"></div>
309
+ </div>
310
+ <div class="opencode-skeleton-item">
311
+ <div class="opencode-skeleton-title"></div>
312
+ <div class="opencode-skeleton-meta"></div>
313
+ </div>
314
+ <div class="opencode-skeleton-item">
315
+ <div class="opencode-skeleton-title"></div>
316
+ <div class="opencode-skeleton-meta"></div>
317
+ </div>
318
+ `;
319
+ sessionList.appendChild(sessionHeaderSkeleton);
320
+ sessionList.appendChild(sessionListHeader);
321
+ sessionList.appendChild(sessionSkeleton);
322
+ sessionList.appendChild(sessionListContent);
323
+ // 折叠/展开会话列表
324
+ let isSessionListCollapsed = true;
325
+ function toggleSessionList() {
326
+ isSessionListCollapsed = !isSessionListCollapsed;
327
+ if (!isSessionListCollapsed) {
328
+ sessionHeaderSkeleton.classList.add('visible');
329
+ sessionListHeader.style.display = 'none';
330
+ sessionSkeleton.classList.add('visible');
331
+ sessionListContent.style.display = 'none';
332
+ }
333
+ sessionList.classList.toggle('collapsed', isSessionListCollapsed);
334
+ toggleBtn.title = isSessionListCollapsed ? '展开会话列表' : '折叠会话列表';
335
+ toggleBtn.setAttribute('aria-label', isSessionListCollapsed ? '展开会话列表' : '折叠会话列表');
336
+ toggleBtn.setAttribute('aria-expanded', String(!isSessionListCollapsed));
337
+ if (!isSessionListCollapsed) {
338
+ setTimeout(() => {
339
+ sessionHeaderSkeleton.classList.remove('visible');
340
+ sessionListHeader.style.display = '';
341
+ sessionSkeleton.classList.remove('visible');
342
+ sessionListContent.style.display = '';
343
+ }, 200);
344
+ }
345
+ }
346
+ toggleBtn.addEventListener('click', toggleSessionList);
347
+ // 创建 iframe 容器
348
+ const iframeContainer = document.createElement('div');
349
+ iframeContainer.className = 'opencode-iframe-container';
350
+ // 创建加载指示器
351
+ const loadingOverlay = document.createElement('div');
352
+ loadingOverlay.className = 'opencode-loading-overlay';
353
+ loadingOverlay.innerHTML = `
354
+ <div class="opencode-loading-spinner"></div>
355
+ <div class="opencode-loading-text">加载中...</div>
356
+ `;
357
+ // 创建 iframe
358
+ const iframe = document.createElement('iframe');
359
+ iframe.className = 'opencode-iframe';
360
+ iframe.src = servicesStarted ? iframeUrl : 'about:blank';
361
+ iframe.allow = 'clipboard-write; clipboard-read';
362
+ iframe.referrerPolicy = 'origin';
363
+ iframe.onload = function () {
364
+ if (servicesStarted) {
365
+ updateContext();
366
+ loadSessions();
367
+ setupSSEConnection();
368
+ }
369
+ hideLoading();
370
+ };
371
+ iframeContainer.appendChild(loadingOverlay);
372
+ iframeContainer.appendChild(iframe);
373
+ // 创建右侧工具栏
374
+ const rightToolbar = document.createElement('div');
375
+ rightToolbar.className = `opencode-right-toolbar${selectedElements.length === 0 ? ' collapsed' : ''}`;
376
+ // 创建已选节点标题
377
+ const selectedNodesHeader = document.createElement('div');
378
+ selectedNodesHeader.className = 'opencode-selected-nodes-header';
379
+ selectedNodesHeader.innerHTML = `
380
+ <div class="opencode-selected-nodes-title">已选节点</div>
381
+ <div class="opencode-selected-nodes-desc">选中的节点会在对话时一起发送给助手</div>
382
+ `;
383
+ // 创建已选节点容器
384
+ const selectedNodesContainer = document.createElement('div');
385
+ selectedNodesContainer.className = 'opencode-selected-nodes';
386
+ selectedNodesContainer.setAttribute('role', 'list');
387
+ selectedNodesContainer.setAttribute('aria-label', '已选元素列表');
388
+ // 创建清空按钮
389
+ const clearAllButton = document.createElement('button');
390
+ clearAllButton.className = 'opencode-clear-all-btn';
391
+ clearAllButton.setAttribute('aria-label', '清空所有已选节点');
392
+ clearAllButton.innerHTML = '一键清空';
393
+ rightToolbar.appendChild(selectedNodesHeader);
394
+ rightToolbar.appendChild(selectedNodesContainer);
395
+ rightToolbar.appendChild(clearAllButton);
396
+ // 创建选择模式常驻提示(固定到页面顶部)
397
+ const selectModeHint = document.createElement('div');
398
+ selectModeHint.className = 'opencode-select-mode-hint';
399
+ selectModeHint.innerHTML = `
400
+ <span>🎯 选择模式已开启 - 点击元素进行选择</span>
401
+ <span class="opencode-hint-shortcut">按 ESC 或 Ctrl+P 退出</span>
402
+ `;
403
+ // 创建元素高亮覆盖层
404
+ const elementHighlight = document.createElement('div');
405
+ elementHighlight.className = 'opencode-element-highlight';
406
+ // 创建元素信息提示框
407
+ const elementTooltip = document.createElement('div');
408
+ elementTooltip.className = 'opencode-element-tooltip';
409
+ // 创建已选节点气泡容器(气泡按钮上方)
410
+ const selectedBubbles = document.createElement('div');
411
+ selectedBubbles.className = 'opencode-selected-bubbles';
412
+ selectedBubbles.setAttribute('role', 'list');
413
+ selectedBubbles.setAttribute('aria-label', '已选元素列表');
414
+ chat.appendChild(sessionList);
415
+ chat.appendChild(chatHeader);
416
+ chat.appendChild(iframeContainer);
417
+ chat.appendChild(rightToolbar);
418
+ container.appendChild(button);
419
+ container.appendChild(selectedBubbles);
420
+ container.appendChild(chat);
421
+ document.body.appendChild(container);
422
+ document.body.appendChild(selectModeHint);
423
+ document.body.appendChild(elementHighlight);
424
+ document.body.appendChild(elementTooltip);
425
+ if (selectedElements.length > 0) {
426
+ renderSelectedNodes();
427
+ }
428
+ /** @type {Array} 会话列表 */
429
+ let sessions = [];
430
+ /** @type {string|null} 当前会话 ID */
431
+ let currentSessionId = null;
432
+ /**
433
+ * 从 URL 中提取会话 ID
434
+ */
435
+ function extractSessionId(url) {
436
+ if (!url)
437
+ return null;
438
+ const match = url.match(/\/session\/([^/?]+)/);
439
+ return match ? match[1] : null;
440
+ }
441
+ // 从初始 URL 中提取会话 ID
442
+ currentSessionId = extractSessionId(sessionUrl);
443
+ /**
444
+ * 显示加载状态
445
+ */
446
+ function showLoading() {
447
+ loadingOverlay.classList.add('visible');
448
+ }
449
+ /**
450
+ * 隐藏加载状态
451
+ */
452
+ function hideLoading() {
453
+ loadingOverlay.classList.remove('visible');
454
+ }
455
+ /**
456
+ * 加载会话列表
457
+ */
458
+ async function loadSessions() {
459
+ try {
460
+ const response = await fetch('/__opencode_sessions__');
461
+ sessions = await response.json();
462
+ renderSessionList();
463
+ }
464
+ catch (e) {
465
+ console.error('Failed to load sessions:', e);
466
+ }
467
+ }
468
+ /**
469
+ * 渲染会话列表
470
+ */
471
+ function renderSessionList() {
472
+ sessionListContent.innerHTML = '';
473
+ const currentProjectSessions = sessions.filter(session => session.directory === cwd);
474
+ currentProjectSessions.forEach(session => {
475
+ const item = document.createElement('div');
476
+ item.className = 'opencode-session-item';
477
+ item.setAttribute('role', 'option');
478
+ item.setAttribute('aria-selected', String(session.id === currentSessionId));
479
+ if (session.id === currentSessionId) {
480
+ item.classList.add('active');
481
+ }
482
+ const header = document.createElement('div');
483
+ header.className = 'opencode-session-header';
484
+ const title = document.createElement('div');
485
+ title.className = 'opencode-session-title';
486
+ title.textContent = session.title || '新会话';
487
+ const deleteBtn = document.createElement('button');
488
+ deleteBtn.className = 'opencode-session-delete-btn';
489
+ deleteBtn.innerHTML = '×';
490
+ deleteBtn.title = '删除会话';
491
+ deleteBtn.setAttribute('aria-label', `删除会话: ${session.title || '新会话'}`);
492
+ deleteBtn.addEventListener('click', (e) => {
493
+ e.stopPropagation();
494
+ confirmDeleteSession(session);
495
+ });
496
+ header.appendChild(title);
497
+ header.appendChild(deleteBtn);
498
+ const meta = document.createElement('div');
499
+ meta.className = 'opencode-session-meta';
500
+ const date = new Date(session.time.updated);
501
+ meta.textContent = date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
502
+ item.appendChild(header);
503
+ item.appendChild(meta);
504
+ item.addEventListener('click', () => {
505
+ switchSession(session);
506
+ });
507
+ sessionListContent.appendChild(item);
508
+ });
509
+ }
510
+ /**
511
+ * 切换会话
512
+ */
513
+ function switchSession(session) {
514
+ if (session.id === currentSessionId) {
515
+ return;
516
+ }
517
+ currentSessionId = session.id;
518
+ const encodedDir = btoa(cwd);
519
+ const baseUrl = iframeUrl.split('/').slice(0, 3).join('/');
520
+ showLoading();
521
+ iframe.src = `${baseUrl}/${encodedDir}/session/${session.id}`;
522
+ renderSessionList();
523
+ }
524
+ /**
525
+ * 创建新会话
526
+ */
527
+ async function createNewSession() {
528
+ try {
529
+ const response = await fetch('/__opencode_sessions__', {
530
+ method: 'POST'
531
+ });
532
+ const newSession = await response.json();
533
+ sessions.unshift(newSession);
534
+ switchSession(newSession);
535
+ }
536
+ catch (e) {
537
+ console.error('Failed to create session:', e);
538
+ showNotification('创建会话失败');
539
+ }
540
+ }
541
+ /**
542
+ * 确认删除会话
543
+ */
544
+ async function confirmDeleteSession(session) {
545
+ const confirmed = await showConfirmDialog(`确定要删除会话 "${session.title || '新会话'}" 吗?`);
546
+ if (confirmed) {
547
+ deleteSession(session);
548
+ }
549
+ }
550
+ /**
551
+ * 删除会话
552
+ */
553
+ async function deleteSession(session) {
554
+ try {
555
+ const response = await fetch(`/__opencode_sessions__?id=${session.id}`, {
556
+ method: 'DELETE'
557
+ });
558
+ if (!response.ok) {
559
+ throw new Error('Delete failed');
560
+ }
561
+ sessions = sessions.filter(s => s.id !== session.id);
562
+ if (session.id === currentSessionId) {
563
+ const remainingSessions = sessions.filter(s => s.directory === cwd);
564
+ if (remainingSessions.length > 0) {
565
+ switchSession(remainingSessions[0]);
566
+ }
567
+ else {
568
+ currentSessionId = null;
569
+ iframe.src = 'about:blank';
570
+ }
571
+ }
572
+ renderSessionList();
573
+ showNotification('会话已删除');
574
+ }
575
+ catch (e) {
576
+ console.error('Failed to delete session:', e);
577
+ showNotification('删除会话失败');
578
+ }
579
+ }
580
+ // 绑定新建会话按钮
581
+ sessionListHeader.querySelector('.opencode-new-session-btn').addEventListener('click', createNewSession);
582
+ /**
583
+ * 应用主题
584
+ */
585
+ function applyTheme() {
586
+ if (theme === 'auto') {
587
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
588
+ container.classList.toggle('opencode-dark', prefersDark);
589
+ }
590
+ else {
591
+ container.classList.toggle('opencode-dark', theme === 'dark');
592
+ }
593
+ }
594
+ applyTheme();
595
+ if (theme === 'auto') {
596
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme);
597
+ }
598
+ /**
599
+ * 切换挂件显示状态
600
+ */
601
+ async function toggle() {
602
+ if (lazy && !servicesStarted) {
603
+ button.classList.add('loading');
604
+ const started = await ensureServicesStarted();
605
+ button.classList.remove('loading');
606
+ if (!started) {
607
+ showNotification('服务启动失败,请检查控制台');
608
+ return;
609
+ }
610
+ }
611
+ if (isSelectMode) {
612
+ exitSelectMode();
613
+ updateContext();
614
+ return;
615
+ }
616
+ isOpen = !isOpen;
617
+ chat.classList.toggle('open', isOpen);
618
+ button.classList.toggle('active', isOpen);
619
+ if (isOpen) {
620
+ updateContext();
621
+ }
622
+ }
623
+ button.addEventListener('click', toggle);
624
+ document.addEventListener('keydown', (e) => {
625
+ if (matchHotkey(e, mainHotkey)) {
626
+ e.preventDefault();
627
+ toggle();
628
+ }
629
+ if (matchHotkey(e, selectHotkey)) {
630
+ e.preventDefault();
631
+ if (window.__VUE_INSPECTOR__) {
632
+ toggleSelectMode();
633
+ }
634
+ else {
635
+ showNotification('Vue Inspector 未加载,无法使用元素选择功能');
636
+ }
637
+ }
638
+ });
639
+ if (open && servicesStarted) {
640
+ setTimeout(() => {
641
+ toggle();
642
+ }, AUTO_OPEN_DELAY);
643
+ }
644
+ /**
645
+ * 显示自定义确认对话框
646
+ * @param {string} message - 确认消息
647
+ * @returns {Promise<boolean>} 用户选择结果
648
+ */
649
+ function showConfirmDialog(message) {
650
+ return new Promise((resolve) => {
651
+ const overlay = document.createElement('div');
652
+ overlay.className = 'opencode-dialog-overlay';
653
+ const dialog = document.createElement('div');
654
+ dialog.className = 'opencode-dialog';
655
+ dialog.setAttribute('role', 'alertdialog');
656
+ dialog.setAttribute('aria-modal', 'true');
657
+ dialog.setAttribute('aria-labelledby', 'opencode-dialog-title');
658
+ dialog.setAttribute('aria-describedby', 'opencode-dialog-desc');
659
+ dialog.innerHTML = `
660
+ <div class="opencode-dialog-content">
661
+ <div id="opencode-dialog-desc" class="opencode-dialog-message">${message}</div>
662
+ </div>
663
+ <div class="opencode-dialog-actions">
664
+ <button class="opencode-dialog-btn cancel" aria-label="取消">取消</button>
665
+ <button class="opencode-dialog-btn confirm" aria-label="确认">确认</button>
666
+ </div>
667
+ `;
668
+ const handleConfirm = () => {
669
+ overlay.remove();
670
+ resolve(true);
671
+ };
672
+ const handleCancel = () => {
673
+ overlay.remove();
674
+ resolve(false);
675
+ };
676
+ dialog.querySelector('.confirm').addEventListener('click', handleConfirm);
677
+ dialog.querySelector('.cancel').addEventListener('click', handleCancel);
678
+ overlay.addEventListener('click', (e) => {
679
+ if (e.target === overlay) {
680
+ handleCancel();
681
+ }
682
+ });
683
+ document.addEventListener('keydown', function handleEscape(e) {
684
+ if (e.key === 'Escape') {
685
+ document.removeEventListener('keydown', handleEscape);
686
+ handleCancel();
687
+ }
688
+ });
689
+ overlay.appendChild(dialog);
690
+ document.body.appendChild(overlay);
691
+ dialog.querySelector('.confirm').focus();
692
+ });
693
+ }
694
+ /**
695
+ * 显示通知
696
+ * @param {string} message - 通知消息
697
+ */
698
+ function showNotification(message) {
699
+ const notification = document.createElement('div');
700
+ notification.className = 'opencode-notification';
701
+ notification.textContent = message;
702
+ chat.appendChild(notification);
703
+ setTimeout(() => {
704
+ notification.remove();
705
+ }, NOTIFICATION_DURATION);
706
+ }
707
+ /**
708
+ * 添加选中元素
709
+ * @param {SelectedElement} elementInfo - 元素信息
710
+ */
711
+ function addElement(elementInfo) {
712
+ const key = elementInfo.filePath && elementInfo.line
713
+ ? `${elementInfo.filePath}:${elementInfo.line}`
714
+ : null;
715
+ const exists = key && selectedElements.some(el => {
716
+ const elKey = el.filePath && el.line ? `${el.filePath}:${el.line}` : null;
717
+ return elKey === key;
718
+ });
719
+ if (!exists) {
720
+ selectedElements.push(elementInfo);
721
+ saveSelectedElements(selectedElements);
722
+ renderSelectedNodes();
723
+ if (isSelectMode) {
724
+ renderSelectedBubbles();
725
+ }
726
+ showNotification(`已选中元素 (${selectedElements.length}个)`);
727
+ }
728
+ else {
729
+ showNotification('该元素已选中');
730
+ }
731
+ }
732
+ /**
733
+ * 移除选中元素
734
+ * @param {number} index - 元素索引
735
+ */
736
+ function removeElement(index) {
737
+ selectedElements.splice(index, 1);
738
+ saveSelectedElements(selectedElements);
739
+ renderSelectedNodes();
740
+ updateToolbarState();
741
+ sendSelectedElements();
742
+ }
743
+ /**
744
+ * 清除所有选中元素
745
+ */
746
+ function clearAllElements() {
747
+ selectedElements = [];
748
+ saveSelectedElements(selectedElements);
749
+ renderSelectedNodes();
750
+ updateToolbarState();
751
+ sendSelectedElements();
752
+ showNotification('已清除所有选中元素');
753
+ }
754
+ /**
755
+ * 更新工具栏状态
756
+ */
757
+ function updateToolbarState() {
758
+ if (selectedElements.length > 0) {
759
+ rightToolbar.classList.remove('collapsed');
760
+ }
761
+ else {
762
+ rightToolbar.classList.add('collapsed');
763
+ }
764
+ }
765
+ /** @type {boolean} 是否处于元素选择模式 */
766
+ let isSelectMode = false;
767
+ /**
768
+ * 处理鼠标移动事件(选择模式下高亮元素)
769
+ * @param {MouseEvent} e - 鼠标事件
770
+ */
771
+ function handleMouseMove(e) {
772
+ if (!isSelectMode)
773
+ return;
774
+ const inspector = window.__VUE_INSPECTOR__;
775
+ if (!inspector)
776
+ return;
777
+ const { targetNode, params } = inspector.getTargetNode(e);
778
+ if (targetNode && params) {
779
+ const rect = targetNode.getBoundingClientRect();
780
+ elementHighlight.style.display = 'block';
781
+ elementHighlight.style.top = `${rect.top}px`;
782
+ elementHighlight.style.left = `${rect.left}px`;
783
+ elementHighlight.style.width = `${rect.width}px`;
784
+ elementHighlight.style.height = `${rect.height}px`;
785
+ const description = getElementDescription(targetNode);
786
+ const fileName = params.file ? params.file.split('/').pop() : '';
787
+ let lineInfo = '';
788
+ if (params.line) {
789
+ lineInfo = `:${params.line}`;
790
+ if (params.column) {
791
+ lineInfo += `:${params.column}`;
792
+ }
793
+ }
794
+ elementTooltip.innerHTML = `
795
+ <div class="opencode-tooltip-tag">${description}</div>
796
+ ${fileName ? `<div class="opencode-tooltip-file">${fileName}${lineInfo}</div>` : ''}
797
+ `;
798
+ elementTooltip.style.display = 'block';
799
+ const tooltipRect = elementTooltip.getBoundingClientRect();
800
+ let tooltipTop = rect.top - tooltipRect.height - 8;
801
+ let tooltipLeft = rect.left;
802
+ if (tooltipTop < 10) {
803
+ tooltipTop = rect.bottom + 8;
804
+ }
805
+ if (tooltipLeft + tooltipRect.width > window.innerWidth - 10) {
806
+ tooltipLeft = window.innerWidth - tooltipRect.width - 10;
807
+ }
808
+ elementTooltip.style.top = `${tooltipTop}px`;
809
+ elementTooltip.style.left = `${tooltipLeft}px`;
810
+ }
811
+ else {
812
+ elementHighlight.style.display = 'none';
813
+ elementTooltip.style.display = 'none';
814
+ }
815
+ }
816
+ /**
817
+ * 切换元素选择模式
818
+ */
819
+ function toggleSelectMode() {
820
+ const inspector = window.__VUE_INSPECTOR__;
821
+ if (!inspector) {
822
+ showNotification('Vue Inspector 未加载,无法使用元素选择功能');
823
+ return;
824
+ }
825
+ isSelectMode = !isSelectMode;
826
+ selectButton.classList.toggle('active', isSelectMode);
827
+ selectButton.setAttribute('aria-pressed', String(isSelectMode));
828
+ selectModeHint.classList.toggle('visible', isSelectMode);
829
+ selectedBubbles.classList.toggle('visible', isSelectMode);
830
+ if (isSelectMode) {
831
+ if (isOpen) {
832
+ isOpen = false;
833
+ chat.classList.remove('open');
834
+ button.classList.remove('active');
835
+ }
836
+ chat.style.display = 'none';
837
+ inspector.enable();
838
+ renderSelectedBubbles();
839
+ document.addEventListener('mousemove', handleMouseMove);
840
+ }
841
+ else {
842
+ chat.style.display = '';
843
+ isOpen = true;
844
+ chat.classList.add('open');
845
+ button.classList.add('active');
846
+ inspector.disable();
847
+ document.removeEventListener('mousemove', handleMouseMove);
848
+ elementHighlight.style.display = 'none';
849
+ elementTooltip.style.display = 'none';
850
+ }
851
+ }
852
+ /**
853
+ * 退出选择模式
854
+ */
855
+ function exitSelectMode() {
856
+ if (!isSelectMode)
857
+ return;
858
+ const inspector = window.__VUE_INSPECTOR__;
859
+ if (inspector) {
860
+ inspector.disable();
861
+ }
862
+ isSelectMode = false;
863
+ selectButton.classList.remove('active');
864
+ selectModeHint.classList.remove('visible');
865
+ selectedBubbles.classList.remove('visible');
866
+ chat.style.display = '';
867
+ isOpen = true;
868
+ chat.classList.add('open');
869
+ button.classList.add('active');
870
+ document.removeEventListener('mousemove', handleMouseMove);
871
+ elementHighlight.style.display = 'none';
872
+ elementTooltip.style.display = 'none';
873
+ }
874
+ // ESC 键退出选择模式(使用捕获阶段确保优先处理)
875
+ document.addEventListener('keydown', (e) => {
876
+ if (e.key === 'Escape' && isSelectMode) {
877
+ e.preventDefault();
878
+ e.stopPropagation();
879
+ exitSelectMode();
880
+ }
881
+ }, true);
882
+ /**
883
+ * 建立 SSE 连接监听服务端事件
884
+ */
885
+ function setupSSEConnection() {
886
+ if (!servicesStarted)
887
+ return;
888
+ const eventSource = new EventSource('/__opencode_events__');
889
+ eventSource.onopen = () => {
890
+ console.log('[OpenCode] SSE connected');
891
+ };
892
+ eventSource.onmessage = (event) => {
893
+ try {
894
+ const data = JSON.parse(event.data);
895
+ console.log('[OpenCode] SSE message:', data);
896
+ if (data.type === 'CONNECTED') {
897
+ // 连接成功后,主动同步当前节点到服务端
898
+ if (selectedElements.length > 0) {
899
+ fetch('/__opencode_context__', {
900
+ method: 'POST',
901
+ headers: { 'Content-Type': 'application/json' },
902
+ body: JSON.stringify({
903
+ url: window.location.href,
904
+ title: document.title,
905
+ selectedElements: selectedElements
906
+ })
907
+ }).catch(() => { });
908
+ }
909
+ }
910
+ if (data.type === 'CLEAR_ELEMENTS' && selectedElements.length > 0) {
911
+ console.log('[OpenCode] Clearing elements');
912
+ selectedElements = [];
913
+ saveSelectedElements(selectedElements);
914
+ renderSelectedNodes();
915
+ updateToolbarState();
916
+ if (isSelectMode) {
917
+ renderSelectedBubbles();
918
+ }
919
+ }
920
+ }
921
+ catch (e) {
922
+ console.error('[OpenCode] SSE parse error:', e);
923
+ }
924
+ };
925
+ eventSource.onerror = (e) => {
926
+ console.error('[OpenCode] SSE error:', e);
927
+ // 连接失败时自动重连
928
+ eventSource.close();
929
+ setTimeout(setupSSEConnection, 3000);
930
+ };
931
+ }
932
+ /**
933
+ * 渲染已选节点气泡
934
+ */
935
+ function renderSelectedBubbles() {
936
+ selectedBubbles.innerHTML = '';
937
+ if (selectedElements.length === 0) {
938
+ selectedBubbles.innerHTML = '<div class="opencode-bubble-empty">暂无选中元素</div>';
939
+ return;
940
+ }
941
+ selectedElements.forEach((element, index) => {
942
+ const bubble = document.createElement('div');
943
+ bubble.className = 'opencode-selected-bubble';
944
+ bubble.setAttribute('role', 'listitem');
945
+ const description = element.description || '未知元素';
946
+ const fileName = element.filePath ? element.filePath.split('/').pop() : '';
947
+ const lineInfo = element.line ? `:${element.line}${element.column ? `:${element.column}` : ''}` : '';
948
+ bubble.innerHTML = `
949
+ <span class="opencode-bubble-text">${description}</span>
950
+ ${fileName ? `<span class="opencode-bubble-file">${fileName}${lineInfo}</span>` : ''}
951
+ <button class="opencode-bubble-remove" data-index="${index}" aria-label="移除元素: ${description}">×</button>
952
+ `;
953
+ bubble.querySelector('.opencode-bubble-remove').addEventListener('click', (e) => {
954
+ e.stopPropagation();
955
+ removeElement(index);
956
+ renderSelectedBubbles();
957
+ });
958
+ selectedBubbles.appendChild(bubble);
959
+ });
960
+ }
961
+ function renderSelectedNodes() {
962
+ selectedNodesContainer.innerHTML = '';
963
+ if (selectedElements.length === 0) {
964
+ rightToolbar.classList.add('collapsed');
965
+ clearAllButton.style.display = 'none';
966
+ }
967
+ else {
968
+ rightToolbar.classList.remove('collapsed');
969
+ clearAllButton.style.display = 'block';
970
+ }
971
+ selectedElements.forEach((element, index) => {
972
+ const node = document.createElement('div');
973
+ node.className = 'opencode-selected-node';
974
+ node.setAttribute('role', 'listitem');
975
+ const description = element.description || '未知元素';
976
+ const textPreview = element.innerText ? element.innerText.substring(0, 30) : '';
977
+ const fileName = element.filePath ? element.filePath.split('/').pop() : '未知文件';
978
+ const lineInfo = element.line ? `:${element.line}${element.column ? `:${element.column}` : ''}` : '';
979
+ node.innerHTML = `
980
+ <div class="opencode-node-content">
981
+ <span class="opencode-node-text">${description}</span>
982
+ <span class="opencode-node-file">${textPreview ? textPreview + ' · ' : ''}${fileName}${lineInfo}</span>
983
+ </div>
984
+ <button class="opencode-node-remove" data-index="${index}" aria-label="移除元素: ${description}">×</button>
985
+ `;
986
+ node.querySelector('.opencode-node-remove').addEventListener('click', (e) => {
987
+ e.stopPropagation();
988
+ removeElement(index);
989
+ });
990
+ node.addEventListener('click', () => {
991
+ highlightElement(element);
992
+ });
993
+ selectedNodesContainer.appendChild(node);
994
+ });
995
+ }
996
+ // 绑定清空按钮点击事件
997
+ clearAllButton.addEventListener('click', async () => {
998
+ if (selectedElements.length === 0)
999
+ return;
1000
+ const confirmed = await showConfirmDialog(`确定要清空所有 ${selectedElements.length} 个已选节点吗?`);
1001
+ if (confirmed) {
1002
+ clearAllElements();
1003
+ }
1004
+ });
1005
+ // 绑定选择按钮点击事件
1006
+ selectButton.addEventListener('click', toggleSelectMode);
1007
+ /**
1008
+ * 高亮页面元素
1009
+ * @param {SelectedElement} element - 元素信息
1010
+ */
1011
+ function highlightElement(element) {
1012
+ try {
1013
+ const description = element.description;
1014
+ if (!description)
1015
+ return;
1016
+ let targetElement = null;
1017
+ if (description.includes('#')) {
1018
+ const idMatch = description.match(/#([^\.\[\s]+)/);
1019
+ if (idMatch) {
1020
+ targetElement = document.getElementById(idMatch[1]);
1021
+ }
1022
+ }
1023
+ if (!targetElement && description.includes('.')) {
1024
+ const classMatch = description.match(/^([a-z]+)\.([^\[\s]+)/i);
1025
+ if (classMatch) {
1026
+ const tagName = classMatch[1];
1027
+ const classes = classMatch[2].split('.').filter(Boolean);
1028
+ const selector = `${tagName}.${classes.join('.')}`;
1029
+ targetElement = document.querySelector(selector);
1030
+ }
1031
+ }
1032
+ if (!targetElement) {
1033
+ const tagMatch = description.match(/^([a-z]+)/i);
1034
+ if (tagMatch) {
1035
+ const simpleSelector = description.split(/[\.\[\s]/)[0];
1036
+ targetElement = document.querySelector(simpleSelector);
1037
+ }
1038
+ }
1039
+ if (targetElement) {
1040
+ targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
1041
+ const highlightOverlay = document.createElement('div');
1042
+ highlightOverlay.className = 'opencode-element-highlight-temp';
1043
+ const rect = targetElement.getBoundingClientRect();
1044
+ highlightOverlay.style.top = `${rect.top + window.scrollY}px`;
1045
+ highlightOverlay.style.left = `${rect.left + window.scrollX}px`;
1046
+ highlightOverlay.style.width = `${rect.width}px`;
1047
+ highlightOverlay.style.height = `${rect.height}px`;
1048
+ document.body.appendChild(highlightOverlay);
1049
+ setTimeout(() => {
1050
+ highlightOverlay.remove();
1051
+ }, 2000);
1052
+ }
1053
+ }
1054
+ catch (e) {
1055
+ console.error('Failed to highlight element:', e);
1056
+ }
1057
+ }
1058
+ /**
1059
+ * 发送选中元素到服务器
1060
+ */
1061
+ function sendSelectedElements() {
1062
+ if (!servicesStarted) {
1063
+ console.log('[OpenCode] sendSelectedElements: services not started');
1064
+ return;
1065
+ }
1066
+ console.log('[OpenCode] Sending selected elements:', selectedElements);
1067
+ fetch('/__opencode_context__', {
1068
+ method: 'POST',
1069
+ headers: { 'Content-Type': 'application/json' },
1070
+ body: JSON.stringify({
1071
+ url: currentPageUrl,
1072
+ title: currentPageTitle,
1073
+ selectedElements: selectedElements
1074
+ })
1075
+ }).then(() => {
1076
+ console.log('[OpenCode] Selected elements sent successfully');
1077
+ }).catch((e) => {
1078
+ console.error('[OpenCode] Failed to send selected elements:', e);
1079
+ });
1080
+ }
1081
+ /**
1082
+ * 截断字符串
1083
+ * @param {string} str - 原字符串
1084
+ * @param {number} maxLength - 最大长度
1085
+ * @returns {string} 截断后的字符串
1086
+ */
1087
+ function truncate(str, maxLength) {
1088
+ if (!str)
1089
+ return '';
1090
+ return str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
1091
+ }
1092
+ /**
1093
+ * 获取元素的直接文本内容
1094
+ * @param {Element} element - DOM 元素
1095
+ * @returns {string} 直接文本内容
1096
+ */
1097
+ function getDirectText(element) {
1098
+ let text = '';
1099
+ for (const child of element.childNodes) {
1100
+ if (child.nodeType === Node.TEXT_NODE) {
1101
+ text += child.textContent || '';
1102
+ }
1103
+ }
1104
+ return text.trim();
1105
+ }
1106
+ /**
1107
+ * 获取元素描述信息
1108
+ * @param {Element} element - DOM 元素
1109
+ * @returns {string} 元素描述
1110
+ */
1111
+ function getElementDescription(element) {
1112
+ const tag = element.tagName.toLowerCase();
1113
+ const parts = [tag];
1114
+ const id = element.id;
1115
+ if (id)
1116
+ parts.push(`#${id}`);
1117
+ const className = element.className && typeof element.className === 'string'
1118
+ ? element.className.trim().split(/\s+/).filter(Boolean).slice(0, 2).join('.')
1119
+ : '';
1120
+ if (className)
1121
+ parts.push(`.${className}`);
1122
+ const name = element.getAttribute('name');
1123
+ if (name)
1124
+ parts.push(`[name="${name}"]`);
1125
+ const placeholder = element.getAttribute('placeholder');
1126
+ if (placeholder)
1127
+ parts.push(`[placeholder="${placeholder.substring(0, 20)}"]`);
1128
+ const src = element.getAttribute('src');
1129
+ if (src)
1130
+ parts.push(`[src]`);
1131
+ const href = element.getAttribute('href');
1132
+ if (href && href !== '#')
1133
+ parts.push(`[href]`);
1134
+ return parts.join('');
1135
+ }
1136
+ /**
1137
+ * 设置 Vue Inspector 钩子
1138
+ */
1139
+ function setupInspectorHook() {
1140
+ if (window.__VUE_INSPECTOR__) {
1141
+ const inspector = window.__VUE_INSPECTOR__;
1142
+ const originalHandleClick = inspector.handleClick.bind(inspector);
1143
+ inspector.handleClick = function (e) {
1144
+ if (isSelectMode) {
1145
+ const { targetNode, params } = inspector.getTargetNode(e);
1146
+ if (targetNode && params) {
1147
+ const innerText = getDirectText(targetNode);
1148
+ const description = getElementDescription(targetNode);
1149
+ const elementInfo = {
1150
+ filePath: params.file,
1151
+ line: params.line,
1152
+ column: params.column,
1153
+ innerText: truncate(innerText, 200),
1154
+ description
1155
+ };
1156
+ addElement(elementInfo);
1157
+ sendSelectedElements();
1158
+ }
1159
+ return;
1160
+ }
1161
+ return originalHandleClick.call(inspector, e);
1162
+ };
1163
+ }
1164
+ }
1165
+ if (window.__VUE_INSPECTOR__) {
1166
+ setupInspectorHook();
1167
+ }
1168
+ else {
1169
+ const checkInspector = setInterval(() => {
1170
+ if (window.__VUE_INSPECTOR__) {
1171
+ setupInspectorHook();
1172
+ clearInterval(checkInspector);
1173
+ }
1174
+ }, INSPECTOR_CHECK_INTERVAL);
1175
+ }
1176
+ // 导出全局 API
1177
+ window.OpenCodeWidget = {
1178
+ open: () => { if (!isOpen)
1179
+ toggle(); },
1180
+ close: () => { if (isOpen)
1181
+ toggle(); },
1182
+ toggle,
1183
+ showNotification,
1184
+ updateContext,
1185
+ };
1186
+ }
1187
+ /**
1188
+ * 构建挂件样式
1189
+ * @returns {string} CSS 样式字符串
1190
+ */
1191
+ function buildWidgetStyles() {
1192
+ return `
1193
+ .opencode-widget {
1194
+ position: fixed;
1195
+ z-index: 999999;
1196
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1197
+ }
1198
+
1199
+ .opencode-widget.bottom-right {
1200
+ bottom: 20px;
1201
+ right: 20px;
1202
+ }
1203
+
1204
+ .opencode-widget.bottom-left {
1205
+ bottom: 20px;
1206
+ left: 20px;
1207
+ }
1208
+
1209
+ .opencode-widget.top-right {
1210
+ top: 20px;
1211
+ right: 20px;
1212
+ }
1213
+
1214
+ .opencode-widget.top-left {
1215
+ top: 20px;
1216
+ left: 20px;
1217
+ }
1218
+
1219
+ .opencode-button {
1220
+ width: 44px;
1221
+ height: 44px;
1222
+ border-radius: 50%;
1223
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1224
+ border: none;
1225
+ cursor: pointer;
1226
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
1227
+ transition: all 0.3s ease;
1228
+ display: flex;
1229
+ align-items: center;
1230
+ justify-content: center;
1231
+ color: white;
1232
+ padding: 0;
1233
+ position: relative;
1234
+ }
1235
+
1236
+ .opencode-button::before {
1237
+ content: '';
1238
+ position: absolute;
1239
+ top: -8px;
1240
+ left: -8px;
1241
+ right: -8px;
1242
+ bottom: -8px;
1243
+ border-radius: 50%;
1244
+ }
1245
+
1246
+ .opencode-button:hover {
1247
+ transform: scale(1.1);
1248
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
1249
+ background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
1250
+ }
1251
+
1252
+ .opencode-button.active {
1253
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
1254
+ box-shadow: 0 6px 20px rgba(240, 147, 251, 0.4);
1255
+ }
1256
+
1257
+ .opencode-button.active svg {
1258
+ transform: rotate(180deg);
1259
+ }
1260
+
1261
+ .opencode-button svg {
1262
+ transition: transform 0.3s ease;
1263
+ }
1264
+
1265
+ .opencode-button.loading {
1266
+ animation: pulse 1s infinite;
1267
+ }
1268
+
1269
+ @keyframes pulse {
1270
+ 0%, 100% { opacity: 1; }
1271
+ 50% { opacity: 0.5; }
1272
+ }
1273
+
1274
+ .opencode-chat {
1275
+ position: absolute;
1276
+ width: 700px;
1277
+ height: 86vh;
1278
+ background: white;
1279
+ border-radius: 16px;
1280
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
1281
+ overflow: hidden;
1282
+ opacity: 0;
1283
+ visibility: hidden;
1284
+ transform: translateY(20px) scale(0.95);
1285
+ transition: all 0.3s ease;
1286
+ display: flex;
1287
+ }
1288
+
1289
+ .opencode-widget.bottom-right .opencode-chat {
1290
+ bottom: 48px;
1291
+ right: 0;
1292
+ }
1293
+
1294
+ .opencode-widget.bottom-left .opencode-chat {
1295
+ bottom: 48px;
1296
+ left: 0;
1297
+ }
1298
+
1299
+ .opencode-widget.top-right .opencode-chat {
1300
+ top: 48px;
1301
+ right: 0;
1302
+ }
1303
+
1304
+ .opencode-widget.top-left .opencode-chat {
1305
+ top: 48px;
1306
+ left: 0;
1307
+ }
1308
+
1309
+ .opencode-chat.open {
1310
+ opacity: 1;
1311
+ visibility: visible;
1312
+ transform: translateY(0) scale(1);
1313
+ }
1314
+
1315
+ .opencode-chat-header {
1316
+ position: absolute;
1317
+ top: 0;
1318
+ left: 0;
1319
+ right: 0;
1320
+ display: flex;
1321
+ align-items: center;
1322
+ justify-content: space-between;
1323
+ padding: 0 12px;
1324
+ height: 40px;
1325
+ background: #f8f9fa;
1326
+ border-bottom: 1px solid #e5e7eb;
1327
+ z-index: 5;
1328
+ }
1329
+
1330
+ .opencode-chat-header-left {
1331
+ display: flex;
1332
+ align-items: center;
1333
+ gap: 4px;
1334
+ }
1335
+
1336
+ .opencode-chat-header-title {
1337
+ font-size: 14px;
1338
+ font-weight: 600;
1339
+ color: #374151;
1340
+ position: absolute;
1341
+ left: 50%;
1342
+ transform: translateX(-50%);
1343
+ }
1344
+
1345
+ .opencode-chat-header-actions {
1346
+ display: flex;
1347
+ gap: 4px;
1348
+ }
1349
+
1350
+ .opencode-header-btn {
1351
+ width: 28px;
1352
+ height: 28px;
1353
+ border-radius: 6px;
1354
+ border: none;
1355
+ background: transparent;
1356
+ color: #6b7280;
1357
+ cursor: pointer;
1358
+ display: flex;
1359
+ align-items: center;
1360
+ justify-content: center;
1361
+ transition: all 0.2s;
1362
+ }
1363
+
1364
+ .opencode-header-btn:hover {
1365
+ background: #e5e7eb;
1366
+ color: #374151;
1367
+ }
1368
+
1369
+ .opencode-header-btn.close:hover {
1370
+ background: #ef4444;
1371
+ color: white;
1372
+ }
1373
+
1374
+ .opencode-header-btn.select-btn.active {
1375
+ background: #3b82f6;
1376
+ color: white;
1377
+ }
1378
+
1379
+ .opencode-dark .opencode-chat-header {
1380
+ background: #111827;
1381
+ border-bottom-color: #374151;
1382
+ }
1383
+
1384
+ .opencode-dark .opencode-chat-header-title {
1385
+ color: #f3f4f6;
1386
+ }
1387
+
1388
+ .opencode-dark .opencode-header-btn {
1389
+ color: #9ca3af;
1390
+ }
1391
+
1392
+ .opencode-dark .opencode-header-btn:hover {
1393
+ background: #374151;
1394
+ color: #f3f4f6;
1395
+ }
1396
+
1397
+ .opencode-dark .opencode-header-btn.close:hover {
1398
+ background: #ef4444;
1399
+ color: white;
1400
+ }
1401
+
1402
+ .opencode-session-list {
1403
+ margin-top: 40px;
1404
+ width: 240px;
1405
+ background: #f8f9fa;
1406
+ border-right: 1px solid #e5e7eb;
1407
+ display: flex;
1408
+ flex-direction: column;
1409
+ flex-shrink: 0;
1410
+ transition: width 0.2s ease;
1411
+ }
1412
+
1413
+ .opencode-session-list.collapsed {
1414
+ width: 0;
1415
+ overflow: hidden;
1416
+ }
1417
+
1418
+ .opencode-session-list.collapsed .opencode-session-list-header,
1419
+ .opencode-session-list.collapsed .opencode-session-list-content {
1420
+ display: none;
1421
+ }
1422
+
1423
+ .opencode-session-list-header {
1424
+ padding: 16px;
1425
+ border-bottom: 1px solid #e5e7eb;
1426
+ display: flex;
1427
+ justify-content: space-between;
1428
+ align-items: center;
1429
+ font-weight: 600;
1430
+ font-size: 14px;
1431
+ color: #374151;
1432
+ }
1433
+
1434
+ .opencode-new-session-btn {
1435
+ width: 28px;
1436
+ height: 28px;
1437
+ border-radius: 6px;
1438
+ border: none;
1439
+ background: #3b82f6;
1440
+ color: white;
1441
+ font-size: 18px;
1442
+ cursor: pointer;
1443
+ display: flex;
1444
+ align-items: center;
1445
+ justify-content: center;
1446
+ transition: all 0.2s;
1447
+ }
1448
+
1449
+ .opencode-new-session-btn:hover {
1450
+ background: #2563eb;
1451
+ transform: scale(1.05);
1452
+ }
1453
+
1454
+ .opencode-session-list-content {
1455
+ flex: 1;
1456
+ overflow-y: auto;
1457
+ padding: 8px;
1458
+ }
1459
+
1460
+ .opencode-session-item {
1461
+ padding: 12px;
1462
+ border-radius: 8px;
1463
+ cursor: pointer;
1464
+ transition: all 0.2s;
1465
+ margin-bottom: 4px;
1466
+ }
1467
+
1468
+ .opencode-session-item:hover {
1469
+ background: #e5e7eb;
1470
+ }
1471
+
1472
+ .opencode-session-item.active {
1473
+ background: #3b82f6;
1474
+ color: white;
1475
+ }
1476
+
1477
+ .opencode-session-title {
1478
+ font-size: 14px;
1479
+ font-weight: 500;
1480
+ margin-bottom: 4px;
1481
+ overflow: hidden;
1482
+ text-overflow: ellipsis;
1483
+ white-space: nowrap;
1484
+ }
1485
+
1486
+ .opencode-session-meta {
1487
+ font-size: 12px;
1488
+ opacity: 0.6;
1489
+ }
1490
+
1491
+ .opencode-session-header {
1492
+ display: flex;
1493
+ justify-content: space-between;
1494
+ align-items: center;
1495
+ margin-bottom: 4px;
1496
+ }
1497
+
1498
+ .opencode-session-delete-btn {
1499
+ width: 20px;
1500
+ height: 20px;
1501
+ border-radius: 4px;
1502
+ border: none;
1503
+ background: transparent;
1504
+ color: #6b7280;
1505
+ font-size: 16px;
1506
+ cursor: pointer;
1507
+ display: flex;
1508
+ align-items: center;
1509
+ justify-content: center;
1510
+ transition: all 0.2s;
1511
+ opacity: 0;
1512
+ flex-shrink: 0;
1513
+ }
1514
+
1515
+ .opencode-session-item:hover .opencode-session-delete-btn {
1516
+ opacity: 1;
1517
+ }
1518
+
1519
+ .opencode-session-delete-btn:hover {
1520
+ background: #ef4444;
1521
+ color: white;
1522
+ }
1523
+
1524
+ .opencode-session-item.active .opencode-session-delete-btn {
1525
+ color: rgba(255, 255, 255, 0.7);
1526
+ }
1527
+
1528
+ .opencode-session-item.active .opencode-session-delete-btn:hover {
1529
+ background: rgba(255, 255, 255, 0.2);
1530
+ color: white;
1531
+ }
1532
+
1533
+ .opencode-session-header-skeleton {
1534
+ padding: 16px;
1535
+ border-bottom: 1px solid #e5e7eb;
1536
+ display: none;
1537
+ justify-content: space-between;
1538
+ align-items: center;
1539
+ }
1540
+
1541
+ .opencode-session-header-skeleton.visible {
1542
+ display: flex;
1543
+ }
1544
+
1545
+ .opencode-skeleton-header-title {
1546
+ height: 18px;
1547
+ width: 80px;
1548
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
1549
+ background-size: 200% 100%;
1550
+ animation: skeleton-loading 1.5s ease-in-out infinite;
1551
+ border-radius: 4px;
1552
+ }
1553
+
1554
+ .opencode-skeleton-header-btn {
1555
+ width: 28px;
1556
+ height: 28px;
1557
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
1558
+ background-size: 200% 100%;
1559
+ animation: skeleton-loading 1.5s ease-in-out infinite;
1560
+ border-radius: 6px;
1561
+ }
1562
+
1563
+ .opencode-session-skeleton {
1564
+ flex: 1;
1565
+ overflow-y: auto;
1566
+ padding: 8px;
1567
+ display: none;
1568
+ }
1569
+
1570
+ .opencode-session-skeleton.visible {
1571
+ display: block;
1572
+ }
1573
+
1574
+ .opencode-skeleton-item {
1575
+ padding: 12px;
1576
+ border-radius: 8px;
1577
+ margin-bottom: 4px;
1578
+ background: white;
1579
+ }
1580
+
1581
+ .opencode-skeleton-title {
1582
+ height: 16px;
1583
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
1584
+ background-size: 200% 100%;
1585
+ animation: skeleton-loading 1.5s ease-in-out infinite;
1586
+ border-radius: 4px;
1587
+ margin-bottom: 8px;
1588
+ width: 70%;
1589
+ }
1590
+
1591
+ .opencode-skeleton-meta {
1592
+ height: 12px;
1593
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
1594
+ background-size: 200% 100%;
1595
+ animation: skeleton-loading 1.5s ease-in-out infinite;
1596
+ border-radius: 4px;
1597
+ width: 50%;
1598
+ }
1599
+
1600
+ @keyframes skeleton-loading {
1601
+ 0% {
1602
+ background-position: 200% 0;
1603
+ }
1604
+ 100% {
1605
+ background-position: -200% 0;
1606
+ }
1607
+ }
1608
+
1609
+ .opencode-dark .opencode-skeleton-item {
1610
+ background: #1f2937;
1611
+ }
1612
+
1613
+ .opencode-dark .opencode-skeleton-title,
1614
+ .opencode-dark .opencode-skeleton-meta {
1615
+ background: linear-gradient(90deg, #374151 25%, #4b5563 50%, #374151 75%);
1616
+ background-size: 200% 100%;
1617
+ }
1618
+
1619
+ .opencode-dark .opencode-session-header-skeleton {
1620
+ border-bottom-color: #374151;
1621
+ }
1622
+
1623
+ .opencode-dark .opencode-skeleton-header-title,
1624
+ .opencode-dark .opencode-skeleton-header-btn {
1625
+ background: linear-gradient(90deg, #374151 25%, #4b5563 50%, #374151 75%);
1626
+ background-size: 200% 100%;
1627
+ }
1628
+
1629
+ .opencode-iframe-container {
1630
+ flex: 1;
1631
+ position: relative;
1632
+ overflow: hidden;
1633
+ display: flex;
1634
+ flex-direction: column;
1635
+ margin-top: -42px;
1636
+ padding-top: 40px;
1637
+ }
1638
+
1639
+ .opencode-loading-overlay {
1640
+ position: absolute;
1641
+ top: 0;
1642
+ left: 0;
1643
+ right: 0;
1644
+ bottom: 0;
1645
+ background: rgba(255, 255, 255, 0.9);
1646
+ display: none;
1647
+ flex-direction: column;
1648
+ align-items: center;
1649
+ justify-content: center;
1650
+ z-index: 10;
1651
+ transition: opacity 0.3s ease;
1652
+ }
1653
+
1654
+ .opencode-loading-overlay.visible {
1655
+ display: flex;
1656
+ }
1657
+
1658
+ .opencode-loading-spinner {
1659
+ width: 40px;
1660
+ height: 40px;
1661
+ border: 3px solid #e5e7eb;
1662
+ border-top-color: #3b82f6;
1663
+ border-radius: 50%;
1664
+ animation: spin 0.8s linear infinite;
1665
+ }
1666
+
1667
+ @keyframes spin {
1668
+ to { transform: rotate(360deg); }
1669
+ }
1670
+
1671
+ .opencode-loading-text {
1672
+ margin-top: 12px;
1673
+ font-size: 14px;
1674
+ color: #6b7280;
1675
+ }
1676
+
1677
+ .opencode-iframe {
1678
+ width: 100%;
1679
+ height: 100%;
1680
+ border: none;
1681
+ }
1682
+
1683
+ .opencode-dark .opencode-chat {
1684
+ background: #1a1a1a;
1685
+ }
1686
+
1687
+ .opencode-dark .opencode-session-list {
1688
+ background: #111827;
1689
+ border-right-color: #374151;
1690
+ }
1691
+
1692
+ .opencode-dark .opencode-session-toggle {
1693
+ color: #9ca3af;
1694
+ border-bottom-color: #374151;
1695
+ }
1696
+
1697
+ .opencode-dark .opencode-session-toggle:hover {
1698
+ background: #374151;
1699
+ color: #f3f4f6;
1700
+ }
1701
+
1702
+ .opencode-dark .opencode-session-list-header {
1703
+ border-bottom-color: #374151;
1704
+ color: #f3f4f6;
1705
+ }
1706
+
1707
+ .opencode-dark .opencode-session-item:hover {
1708
+ background: #374151;
1709
+ }
1710
+
1711
+ .opencode-dark .opencode-session-item.active {
1712
+ background: #3b82f6;
1713
+ }
1714
+
1715
+ .opencode-dark .opencode-loading-overlay {
1716
+ background: rgba(26, 26, 26, 0.9);
1717
+ }
1718
+
1719
+ .opencode-dark .opencode-loading-spinner {
1720
+ border-color: #374151;
1721
+ border-top-color: #3b82f6;
1722
+ }
1723
+
1724
+ .opencode-dark .opencode-loading-text {
1725
+ color: #9ca3af;
1726
+ }
1727
+
1728
+ .opencode-right-toolbar {
1729
+ width: 140px;
1730
+ background: #f8f9fa;
1731
+ border-left: 1px solid #e5e7eb;
1732
+ display: flex;
1733
+ flex-direction: column;
1734
+ flex-shrink: 0;
1735
+ transition: width 0.2s ease;
1736
+ margin-top: 40px;
1737
+ overflow: hidden;
1738
+ }
1739
+
1740
+ .opencode-right-toolbar.collapsed {
1741
+ width: 0;
1742
+ overflow: hidden;
1743
+ }
1744
+
1745
+ .opencode-right-toolbar.collapsed .opencode-selected-nodes-header,
1746
+ .opencode-right-toolbar.collapsed .opencode-selected-nodes,
1747
+ .opencode-right-toolbar.collapsed .opencode-clear-all-btn {
1748
+ display: none;
1749
+ }
1750
+
1751
+ .opencode-selected-nodes-header {
1752
+ padding: 12px 8px 8px;
1753
+ border-bottom: 1px solid #e5e7eb;
1754
+ }
1755
+
1756
+ .opencode-selected-nodes-title {
1757
+ font-size: 14px;
1758
+ font-weight: 600;
1759
+ color: #374151;
1760
+ margin-bottom: 4px;
1761
+ }
1762
+
1763
+ .opencode-selected-nodes-desc {
1764
+ font-size: 11px;
1765
+ color: #9ca3af;
1766
+ line-height: 1.4;
1767
+ }
1768
+
1769
+ .opencode-selected-nodes {
1770
+ flex: 1;
1771
+ display: flex;
1772
+ flex-direction: column;
1773
+ padding: 8px;
1774
+ gap: 6px;
1775
+ overflow-y: auto;
1776
+ overflow-x: hidden;
1777
+ }
1778
+
1779
+ .opencode-selected-nodes:empty::before {
1780
+ content: '暂无选中元素';
1781
+ color: #9ca3af;
1782
+ font-size: 12px;
1783
+ text-align: center;
1784
+ padding: 20px 10px;
1785
+ }
1786
+
1787
+ .opencode-selected-node {
1788
+ display: flex;
1789
+ align-items: center;
1790
+ gap: 8px;
1791
+ padding: 8px 10px;
1792
+ background: white;
1793
+ border: 1px solid #e5e7eb;
1794
+ border-radius: 6px;
1795
+ font-size: 12px;
1796
+ transition: all 0.2s;
1797
+ }
1798
+
1799
+ .opencode-selected-node:hover {
1800
+ border-color: #3b82f6;
1801
+ box-shadow: 0 2px 4px rgba(59, 130, 246, 0.1);
1802
+ }
1803
+
1804
+ .opencode-node-content {
1805
+ flex: 1;
1806
+ min-width: 0;
1807
+ display: flex;
1808
+ flex-direction: column;
1809
+ gap: 2px;
1810
+ }
1811
+
1812
+ .opencode-node-text {
1813
+ color: #374151;
1814
+ font-weight: 500;
1815
+ overflow: hidden;
1816
+ text-overflow: ellipsis;
1817
+ white-space: nowrap;
1818
+ }
1819
+
1820
+ .opencode-node-file {
1821
+ color: #9ca3af;
1822
+ font-size: 11px;
1823
+ overflow: hidden;
1824
+ text-overflow: ellipsis;
1825
+ white-space: nowrap;
1826
+ }
1827
+
1828
+ .opencode-node-remove {
1829
+ width: 18px;
1830
+ height: 18px;
1831
+ border-radius: 4px;
1832
+ border: none;
1833
+ background: transparent;
1834
+ color: #9ca3af;
1835
+ cursor: pointer;
1836
+ display: flex;
1837
+ align-items: center;
1838
+ justify-content: center;
1839
+ font-size: 14px;
1840
+ transition: all 0.2s;
1841
+ flex-shrink: 0;
1842
+ }
1843
+
1844
+ .opencode-node-remove:hover {
1845
+ background: #ef4444;
1846
+ color: white;
1847
+ }
1848
+
1849
+ .opencode-clear-all-btn {
1850
+ width: calc(100% - 16px);
1851
+ margin: 8px;
1852
+ padding: 8px 12px;
1853
+ border-radius: 6px;
1854
+ border: none;
1855
+ background: #ef4444;
1856
+ color: white;
1857
+ font-size: 12px;
1858
+ font-weight: 500;
1859
+ cursor: pointer;
1860
+ display: flex;
1861
+ align-items: center;
1862
+ justify-content: center;
1863
+ gap: 4px;
1864
+ transition: all 0.2s;
1865
+ }
1866
+
1867
+ .opencode-clear-all-btn:hover {
1868
+ background: #dc2626;
1869
+ transform: scale(1.02);
1870
+ }
1871
+
1872
+ .opencode-dark .opencode-right-toolbar {
1873
+ background: #111827;
1874
+ border-left-color: #374151;
1875
+ }
1876
+
1877
+ .opencode-dark .opencode-selected-nodes-header {
1878
+ border-bottom-color: #374151;
1879
+ }
1880
+
1881
+ .opencode-dark .opencode-selected-nodes-title {
1882
+ color: #f3f4f6;
1883
+ }
1884
+
1885
+ .opencode-dark .opencode-selected-nodes-desc {
1886
+ color: #6b7280;
1887
+ }
1888
+
1889
+ .opencode-dark .opencode-selected-nodes:empty::before {
1890
+ color: #6b7280;
1891
+ }
1892
+
1893
+ .opencode-dark .opencode-selected-node {
1894
+ background: #1f2937;
1895
+ border-color: #374151;
1896
+ }
1897
+
1898
+ .opencode-dark .opencode-selected-node:hover {
1899
+ border-color: #3b82f6;
1900
+ }
1901
+
1902
+ .opencode-dark .opencode-node-text {
1903
+ color: #f3f4f6;
1904
+ }
1905
+
1906
+ .opencode-dark .opencode-node-file {
1907
+ color: #6b7280;
1908
+ }
1909
+
1910
+ .opencode-dark .opencode-node-remove {
1911
+ color: #6b7280;
1912
+ }
1913
+
1914
+ .opencode-dark .opencode-node-remove:hover {
1915
+ background: #ef4444;
1916
+ color: white;
1917
+ }
1918
+
1919
+ .opencode-dark .opencode-clear-all-btn {
1920
+ background: #dc2626;
1921
+ color: white;
1922
+ }
1923
+
1924
+ .opencode-dark .opencode-clear-all-btn:hover {
1925
+ background: #b91c1c;
1926
+ }
1927
+
1928
+ .opencode-dark .opencode-button {
1929
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1930
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.5);
1931
+ }
1932
+
1933
+ .opencode-notification {
1934
+ position: absolute;
1935
+ top: 20px;
1936
+ left: 50%;
1937
+ transform: translateX(-50%);
1938
+ padding: 12px 20px;
1939
+ background: #10b981;
1940
+ color: white;
1941
+ border-radius: 8px;
1942
+ font-size: 14px;
1943
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1944
+ animation: slideDown 0.3s ease;
1945
+ z-index: 10;
1946
+ }
1947
+
1948
+ .opencode-dialog-overlay {
1949
+ position: fixed;
1950
+ top: 0;
1951
+ left: 0;
1952
+ right: 0;
1953
+ bottom: 0;
1954
+ background: rgba(0, 0, 0, 0.5);
1955
+ display: flex;
1956
+ align-items: center;
1957
+ justify-content: center;
1958
+ z-index: 9999999;
1959
+ animation: fadeIn 0.2s ease;
1960
+ }
1961
+
1962
+ @keyframes fadeIn {
1963
+ from { opacity: 0; }
1964
+ to { opacity: 1; }
1965
+ }
1966
+
1967
+ .opencode-dialog {
1968
+ background: white;
1969
+ border-radius: 12px;
1970
+ padding: 24px;
1971
+ min-width: 320px;
1972
+ max-width: 400px;
1973
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
1974
+ animation: scaleIn 0.2s ease;
1975
+ }
1976
+
1977
+ @keyframes scaleIn {
1978
+ from { transform: scale(0.9); opacity: 0; }
1979
+ to { transform: scale(1); opacity: 1; }
1980
+ }
1981
+
1982
+ .opencode-dialog-content {
1983
+ margin-bottom: 20px;
1984
+ }
1985
+
1986
+ .opencode-dialog-message {
1987
+ font-size: 15px;
1988
+ color: #374151;
1989
+ line-height: 1.5;
1990
+ }
1991
+
1992
+ .opencode-dialog-actions {
1993
+ display: flex;
1994
+ gap: 12px;
1995
+ justify-content: flex-end;
1996
+ }
1997
+
1998
+ .opencode-dialog-btn {
1999
+ padding: 10px 20px;
2000
+ border-radius: 8px;
2001
+ border: none;
2002
+ font-size: 14px;
2003
+ font-weight: 500;
2004
+ cursor: pointer;
2005
+ transition: all 0.2s;
2006
+ }
2007
+
2008
+ .opencode-dialog-btn.cancel {
2009
+ background: #f3f4f6;
2010
+ color: #374151;
2011
+ }
2012
+
2013
+ .opencode-dialog-btn.cancel:hover {
2014
+ background: #e5e7eb;
2015
+ }
2016
+
2017
+ .opencode-dialog-btn.confirm {
2018
+ background: #ef4444;
2019
+ color: white;
2020
+ }
2021
+
2022
+ .opencode-dialog-btn.confirm:hover {
2023
+ background: #dc2626;
2024
+ }
2025
+
2026
+ .opencode-dark .opencode-dialog {
2027
+ background: #1f2937;
2028
+ }
2029
+
2030
+ .opencode-dark .opencode-dialog-message {
2031
+ color: #f3f4f6;
2032
+ }
2033
+
2034
+ .opencode-dark .opencode-dialog-btn.cancel {
2035
+ background: #374151;
2036
+ color: #f3f4f6;
2037
+ }
2038
+
2039
+ .opencode-dark .opencode-dialog-btn.cancel:hover {
2040
+ background: #4b5563;
2041
+ }
2042
+
2043
+ @keyframes slideDown {
2044
+ from {
2045
+ transform: translateX(-50%) translateY(-100%);
2046
+ opacity: 0;
2047
+ }
2048
+ to {
2049
+ transform: translateX(-50%) translateY(0);
2050
+ opacity: 1;
2051
+ }
2052
+ }
2053
+
2054
+ .opencode-select-mode-hint {
2055
+ position: fixed;
2056
+ top: 20px;
2057
+ left: 50%;
2058
+ transform: translateX(-50%);
2059
+ padding: 10px 16px;
2060
+ background: #ef4444;
2061
+ color: white;
2062
+ border-radius: 8px;
2063
+ font-size: 13px;
2064
+ box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
2065
+ z-index: 9999999;
2066
+ display: none;
2067
+ align-items: center;
2068
+ gap: 12px;
2069
+ }
2070
+
2071
+ .opencode-select-mode-hint.visible {
2072
+ display: flex;
2073
+ animation: slideDown 0.3s ease;
2074
+ }
2075
+
2076
+ .opencode-hint-shortcut {
2077
+ padding: 2px 6px;
2078
+ background: rgba(255, 255, 255, 0.2);
2079
+ border-radius: 4px;
2080
+ font-size: 12px;
2081
+ }
2082
+
2083
+ .opencode-element-highlight {
2084
+ position: fixed;
2085
+ pointer-events: none;
2086
+ border: 2px solid #3b82f6;
2087
+ background: rgba(59, 130, 246, 0.1);
2088
+ z-index: 9999998;
2089
+ display: none;
2090
+ transition: all 0.1s ease;
2091
+ border-radius: 4px;
2092
+ }
2093
+
2094
+ #vue-inspector-container {
2095
+ display: none !important;
2096
+ }
2097
+
2098
+ .opencode-element-tooltip {
2099
+ position: fixed;
2100
+ background: #1f2937;
2101
+ color: white;
2102
+ padding: 8px 12px;
2103
+ border-radius: 6px;
2104
+ font-size: 12px;
2105
+ z-index: 9999998;
2106
+ display: none;
2107
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
2108
+ max-width: 300px;
2109
+ pointer-events: none;
2110
+ }
2111
+
2112
+ .opencode-tooltip-tag {
2113
+ font-weight: 500;
2114
+ margin-bottom: 4px;
2115
+ word-break: break-all;
2116
+ }
2117
+
2118
+ .opencode-tooltip-file {
2119
+ font-size: 11px;
2120
+ color: #9ca3af;
2121
+ word-break: break-all;
2122
+ }
2123
+
2124
+ .opencode-selected-bubbles {
2125
+ position: absolute;
2126
+ bottom: 44px;
2127
+ right: 0;
2128
+ display: none;
2129
+ flex-direction: column;
2130
+ gap: 6px;
2131
+ max-width: 220px;
2132
+ max-height: 300px;
2133
+ overflow-y: auto;
2134
+ }
2135
+
2136
+ .opencode-selected-bubbles.visible {
2137
+ display: flex;
2138
+ }
2139
+
2140
+ .opencode-selected-bubble {
2141
+ display: flex;
2142
+ flex-direction: column;
2143
+ gap: 2px;
2144
+ padding: 8px 10px;
2145
+ background: white;
2146
+ border: 1px solid #e5e7eb;
2147
+ border-radius: 8px;
2148
+ font-size: 12px;
2149
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
2150
+ position: relative;
2151
+ }
2152
+
2153
+ .opencode-bubble-text {
2154
+ color: #374151;
2155
+ font-weight: 500;
2156
+ overflow: hidden;
2157
+ text-overflow: ellipsis;
2158
+ white-space: nowrap;
2159
+ }
2160
+
2161
+ .opencode-bubble-file {
2162
+ color: #9ca3af;
2163
+ font-size: 11px;
2164
+ overflow: hidden;
2165
+ text-overflow: ellipsis;
2166
+ white-space: nowrap;
2167
+ }
2168
+
2169
+ .opencode-bubble-remove {
2170
+ position: absolute;
2171
+ top: 4px;
2172
+ right: 4px;
2173
+ width: 16px;
2174
+ height: 16px;
2175
+ border-radius: 50%;
2176
+ border: none;
2177
+ background: transparent;
2178
+ color: #9ca3af;
2179
+ cursor: pointer;
2180
+ display: flex;
2181
+ align-items: center;
2182
+ justify-content: center;
2183
+ font-size: 12px;
2184
+ transition: all 0.2s;
2185
+ }
2186
+
2187
+ .opencode-bubble-remove:hover {
2188
+ background: #ef4444;
2189
+ color: white;
2190
+ }
2191
+
2192
+ .opencode-bubble-empty {
2193
+ padding: 8px 12px;
2194
+ background: white;
2195
+ border: 1px dashed #d1d5db;
2196
+ border-radius: 8px;
2197
+ color: #9ca3af;
2198
+ font-size: 12px;
2199
+ text-align: center;
2200
+ }
2201
+
2202
+ .opencode-dark .opencode-selected-bubble {
2203
+ background: #1f2937;
2204
+ border-color: #374151;
2205
+ }
2206
+
2207
+ .opencode-dark .opencode-bubble-text {
2208
+ color: #f3f4f6;
2209
+ }
2210
+
2211
+ .opencode-dark .opencode-bubble-file {
2212
+ color: #6b7280;
2213
+ }
2214
+
2215
+ .opencode-dark .opencode-bubble-remove {
2216
+ color: #6b7280;
2217
+ }
2218
+
2219
+ .opencode-dark .opencode-bubble-empty {
2220
+ background: #1f2937;
2221
+ border-color: #374151;
2222
+ color: #6b7280;
2223
+ }
2224
+
2225
+ .opencode-element-highlight-temp {
2226
+ position: absolute;
2227
+ pointer-events: none;
2228
+ border: 3px solid #3b82f6;
2229
+ background: rgba(59, 130, 246, 0.1);
2230
+ z-index: 9999;
2231
+ border-radius: 4px;
2232
+ animation: highlight-pulse 2s ease-out forwards;
2233
+ }
2234
+
2235
+ @keyframes highlight-pulse {
2236
+ 0% {
2237
+ opacity: 1;
2238
+ transform: scale(1);
2239
+ }
2240
+ 50% {
2241
+ opacity: 0.8;
2242
+ transform: scale(1.02);
2243
+ }
2244
+ 100% {
2245
+ opacity: 0;
2246
+ transform: scale(1);
2247
+ }
2248
+ }
2249
+
2250
+ @media (max-width: 768px) {
2251
+ .opencode-chat {
2252
+ width: calc(100vw - 40px);
2253
+ height: calc(100vh - 100px);
2254
+ }
2255
+ }
2256
+ `;
2257
+ }
2258
+ /**
2259
+ * 自动初始化挂件
2260
+ */
2261
+ function autoInit() {
2262
+ const script = document.currentScript || document.querySelector('script[data-opencode-config]');
2263
+ if (script) {
2264
+ const configBase64 = script.getAttribute('data-opencode-config');
2265
+ if (configBase64) {
2266
+ try {
2267
+ const config = JSON.parse(atob(configBase64));
2268
+ if (document.readyState === 'loading') {
2269
+ document.addEventListener('DOMContentLoaded', function () {
2270
+ initOpenCodeWidget(config);
2271
+ });
2272
+ }
2273
+ else {
2274
+ initOpenCodeWidget(config);
2275
+ }
2276
+ }
2277
+ catch (e) {
2278
+ console.error('[OpenCode Widget] Failed to parse config:', e);
2279
+ }
2280
+ }
2281
+ }
2282
+ }
2283
+ // 导出全局初始化函数
2284
+ window.initOpenCodeWidget = initOpenCodeWidget;
2285
+ // 自动初始化
2286
+ autoInit();
2287
+ })();
2288
+ //# sourceMappingURL=client.js.map