vite-plugin-opencode-assistant 1.0.3 → 1.0.4

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.
@@ -4,11 +4,11 @@
4
4
  * @description 用于在浏览器中显示 OpenCode AI 助手挂件
5
5
  */
6
6
  (function () {
7
- 'use strict';
7
+ "use strict";
8
8
  /** @type {string} 初始化标记 */
9
- const INIT_MARKER = '__OPENCODE_INITIALIZED__';
9
+ const INIT_MARKER = "__OPENCODE_INITIALIZED__";
10
10
  /** @type {string} 选中元素存储键 */
11
- const SELECTED_ELEMENTS_KEY = '__opencode_selected_elements__';
11
+ const SELECTED_ELEMENTS_KEY = "__opencode_selected_elements__";
12
12
  /** @type {number} 服务器同步间隔(毫秒) */
13
13
  const SERVER_SYNC_INTERVAL = 2000;
14
14
  /** @type {number} 检查 Vue Inspector 间隔(毫秒) */
@@ -50,11 +50,26 @@
50
50
  if (window[INIT_MARKER])
51
51
  return;
52
52
  window[INIT_MARKER] = true;
53
- const { webUrl, position, theme, open, sessionUrl, lazy, hotkey, cwd } = config;
53
+ const { webUrl, position, theme, open, sessionUrl: initialSessionUrl, lazy, hotkey, cwd, } = config;
54
+ /** @type {string|undefined} 会话 URL */
55
+ let sessionUrl = initialSessionUrl;
56
+ /** @type {string|null} 当前会话 ID */
57
+ let currentSessionId = null;
58
+ /**
59
+ * 从 URL 中提取会话 ID
60
+ */
61
+ function extractSessionId(url) {
62
+ if (!url)
63
+ return null;
64
+ const match = url.match(/\/session\/([^/?]+)/);
65
+ return match ? match[1] : null;
66
+ }
67
+ // 从初始 URL 中提取会话 ID
68
+ currentSessionId = extractSessionId(sessionUrl);
54
69
  /** @type {string} 当前页面 URL */
55
- let currentPageUrl = '';
70
+ let currentPageUrl = "";
56
71
  /** @type {string} 当前页面标题 */
57
- let currentPageTitle = '';
72
+ let currentPageTitle = "";
58
73
  /** @type {boolean} 服务是否已启动 */
59
74
  let servicesStarted = !lazy;
60
75
  /** @type {boolean} 挂件是否打开 */
@@ -66,20 +81,22 @@
66
81
  */
67
82
  function parseHotkey(hotkeyStr) {
68
83
  if (!hotkeyStr)
69
- return { ctrl: true, shift: false, alt: false, key: 'k' };
70
- const parts = hotkeyStr.toLowerCase().split('+');
84
+ return { ctrl: true, shift: false, alt: false, key: "k" };
85
+ const parts = hotkeyStr.toLowerCase().split("+");
71
86
  const key = parts.pop();
72
87
  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'
88
+ ctrl: parts.includes("ctrl") ||
89
+ parts.includes("cmd") ||
90
+ parts.includes("meta"),
91
+ shift: parts.includes("shift"),
92
+ alt: parts.includes("alt"),
93
+ key: key || "k",
77
94
  };
78
95
  }
79
96
  /** @type {HotkeyConfig} 主快捷键配置 */
80
97
  const mainHotkey = parseHotkey(hotkey);
81
98
  /** @type {HotkeyConfig} 选择模式快捷键配置 */
82
- const selectHotkey = parseHotkey('ctrl+p');
99
+ const selectHotkey = parseHotkey("ctrl+p");
83
100
  /**
84
101
  * 检查键盘事件是否匹配快捷键
85
102
  * @param {KeyboardEvent} e - 键盘事件
@@ -87,7 +104,9 @@
87
104
  * @returns {boolean} 是否匹配
88
105
  */
89
106
  function matchHotkey(e, hotkeyConfig) {
90
- const ctrlMatch = hotkeyConfig.ctrl ? (e.ctrlKey || e.metaKey) : !(e.ctrlKey || e.metaKey);
107
+ const ctrlMatch = hotkeyConfig.ctrl
108
+ ? e.ctrlKey || e.metaKey
109
+ : !(e.ctrlKey || e.metaKey);
91
110
  const shiftMatch = hotkeyConfig.shift ? e.shiftKey : !e.shiftKey;
92
111
  const altMatch = hotkeyConfig.alt ? e.altKey : !e.altKey;
93
112
  const keyMatch = e.key.toLowerCase() === hotkeyConfig.key.toLowerCase();
@@ -131,18 +150,26 @@
131
150
  if (servicesStarted)
132
151
  return true;
133
152
  try {
134
- const res = await fetch('/__opencode_start__');
153
+ const res = await fetch("/__opencode_start__");
135
154
  const data = await res.json();
136
155
  if (data.success) {
137
156
  servicesStarted = true;
138
- if (data.sessionUrl && iframe) {
139
- iframe.src = data.sessionUrl;
157
+ if (data.sessionUrl) {
158
+ sessionUrl = data.sessionUrl;
159
+ currentSessionId = extractSessionId(sessionUrl);
160
+ if (iframe)
161
+ iframe.src = sessionUrl;
162
+ }
163
+ else {
164
+ if (iframe)
165
+ iframe.src = "about:blank";
166
+ showLoading();
140
167
  }
141
168
  return true;
142
169
  }
143
170
  }
144
171
  catch (e) {
145
- console.error('[OpenCode Widget] Failed to start services:', e);
172
+ console.error("[OpenCode Widget] Failed to start services:", e);
146
173
  }
147
174
  return false;
148
175
  }
@@ -158,10 +185,14 @@
158
185
  if (force || newUrl !== currentPageUrl || newTitle !== currentPageTitle) {
159
186
  currentPageUrl = newUrl;
160
187
  currentPageTitle = newTitle;
161
- fetch('/__opencode_context__', {
162
- method: 'POST',
163
- headers: { 'Content-Type': 'application/json' },
164
- body: JSON.stringify({ url: newUrl, title: newTitle, selectedElements })
188
+ fetch("/__opencode_context__", {
189
+ method: "POST",
190
+ headers: { "Content-Type": "application/json" },
191
+ body: JSON.stringify({
192
+ url: newUrl,
193
+ title: newTitle,
194
+ selectedElements,
195
+ }),
165
196
  }).catch(() => { });
166
197
  }
167
198
  }
@@ -176,8 +207,8 @@
176
207
  originalReplaceState.apply(this, args);
177
208
  setTimeout(updateContext, 0);
178
209
  };
179
- window.addEventListener('popstate', () => setTimeout(updateContext, 0));
180
- window.addEventListener('hashchange', () => setTimeout(updateContext, 0));
210
+ window.addEventListener("popstate", () => setTimeout(updateContext, 0));
211
+ window.addEventListener("hashchange", () => setTimeout(updateContext, 0));
181
212
  // 监听标题变化
182
213
  const titleObserver = new MutationObserver(() => {
183
214
  if (document.title !== currentPageTitle) {
@@ -187,113 +218,114 @@
187
218
  if (document.head) {
188
219
  titleObserver.observe(document.head, { childList: true, subtree: true });
189
220
  }
221
+ /** @type {EventSource|null} SSE 连接实例 */
222
+ let sseConnection = null;
190
223
  if (servicesStarted) {
191
224
  updateContext(true);
225
+ setupSSEConnection();
192
226
  }
193
- /** @type {string} iframe URL */
194
- const iframeUrl = sessionUrl || webUrl;
195
227
  // 创建样式
196
- const style = document.createElement('style');
228
+ const style = document.createElement("style");
197
229
  style.textContent = buildWidgetStyles();
198
230
  document.head.appendChild(style);
199
231
  // 创建容器
200
- const container = document.createElement('div');
232
+ const container = document.createElement("div");
201
233
  container.className = `opencode-widget ${position}`;
202
234
  // 创建按钮
203
- const button = document.createElement('button');
204
- button.className = 'opencode-button';
235
+ const button = document.createElement("button");
236
+ button.className = "opencode-button";
205
237
  button.innerHTML = `
206
238
  <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
239
  `;
208
- button.setAttribute('aria-label', '打开 AI 助手');
209
- button.title = `AI 助手 (${hotkey || 'Ctrl+K'})`;
240
+ button.setAttribute("aria-label", "打开 AI 助手");
241
+ button.title = `AI 助手 (${hotkey || "Ctrl+K"})`;
210
242
  // 创建聊天面板
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 助手对话窗口');
243
+ const chat = document.createElement("div");
244
+ chat.className = "opencode-chat";
245
+ chat.setAttribute("role", "dialog");
246
+ chat.setAttribute("aria-modal", "true");
247
+ chat.setAttribute("aria-label", "AI 助手对话窗口");
216
248
  // 创建面板头部操作栏
217
- const chatHeader = document.createElement('div');
218
- chatHeader.className = 'opencode-chat-header';
249
+ const chatHeader = document.createElement("div");
250
+ chatHeader.className = "opencode-chat-header";
219
251
  // 左侧操作区
220
- const headerLeft = document.createElement('div');
221
- headerLeft.className = 'opencode-chat-header-left';
252
+ const headerLeft = document.createElement("div");
253
+ headerLeft.className = "opencode-chat-header-left";
222
254
  // 会话列表折叠按钮
223
- const toggleBtn = document.createElement('button');
224
- toggleBtn.className = 'opencode-header-btn session-toggle';
255
+ const toggleBtn = document.createElement("button");
256
+ toggleBtn.className = "opencode-header-btn session-toggle";
225
257
  toggleBtn.innerHTML = `
226
258
  <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
227
259
  <path d="M4 6h16M4 12h16M4 18h16" stroke-linecap="round"/>
228
260
  </svg>
229
261
  `;
230
- toggleBtn.title = '展开会话列表';
231
- toggleBtn.setAttribute('aria-label', '展开会话列表');
232
- toggleBtn.setAttribute('aria-expanded', 'false');
262
+ toggleBtn.title = "展开会话列表";
263
+ toggleBtn.setAttribute("aria-label", "展开会话列表");
264
+ toggleBtn.setAttribute("aria-expanded", "false");
233
265
  // 选择元素按钮
234
- const selectButton = document.createElement('button');
235
- selectButton.className = 'opencode-header-btn select-btn';
266
+ const selectButton = document.createElement("button");
267
+ selectButton.className = "opencode-header-btn select-btn";
236
268
  selectButton.innerHTML = `
237
269
  <svg viewBox="0 0 1024 1024" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
238
270
  <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
271
  </svg>
240
272
  `;
241
- selectButton.title = '选择页面元素 (Ctrl+P)';
242
- selectButton.setAttribute('aria-label', '选择页面元素');
243
- selectButton.setAttribute('aria-pressed', 'false');
273
+ selectButton.title = "选择页面元素 (Ctrl+P)";
274
+ selectButton.setAttribute("aria-label", "选择页面元素");
275
+ selectButton.setAttribute("aria-pressed", "false");
244
276
  headerLeft.appendChild(toggleBtn);
245
277
  headerLeft.appendChild(selectButton);
246
278
  // 标题
247
- const headerTitle = document.createElement('span');
248
- headerTitle.className = 'opencode-chat-header-title';
249
- headerTitle.textContent = 'AI 助手';
279
+ const headerTitle = document.createElement("span");
280
+ headerTitle.className = "opencode-chat-header-title";
281
+ headerTitle.textContent = "AI 助手";
250
282
  // 右侧操作区
251
- const headerActions = document.createElement('div');
252
- headerActions.className = 'opencode-chat-header-actions';
283
+ const headerActions = document.createElement("div");
284
+ headerActions.className = "opencode-chat-header-actions";
253
285
  // 关闭按钮
254
- const closeBtn = document.createElement('button');
255
- closeBtn.className = 'opencode-header-btn close';
286
+ const closeBtn = document.createElement("button");
287
+ closeBtn.className = "opencode-header-btn close";
256
288
  closeBtn.innerHTML = `
257
289
  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
258
290
  <path d="M18 6L6 18M6 6l12 12"/>
259
291
  </svg>
260
292
  `;
261
- closeBtn.title = '关闭';
262
- closeBtn.setAttribute('aria-label', '关闭面板');
263
- closeBtn.addEventListener('click', () => {
293
+ closeBtn.title = "关闭";
294
+ closeBtn.setAttribute("aria-label", "关闭面板");
295
+ closeBtn.addEventListener("click", () => {
264
296
  isOpen = false;
265
- chat.classList.remove('open');
266
- button.classList.remove('active');
297
+ chat.classList.remove("open");
298
+ button.classList.remove("active");
267
299
  });
268
300
  headerActions.appendChild(closeBtn);
269
301
  chatHeader.appendChild(headerLeft);
270
302
  chatHeader.appendChild(headerTitle);
271
303
  chatHeader.appendChild(headerActions);
272
304
  // 创建会话列表
273
- const sessionList = document.createElement('div');
274
- sessionList.className = 'opencode-session-list collapsed';
305
+ const sessionList = document.createElement("div");
306
+ sessionList.className = "opencode-session-list collapsed";
275
307
  // 创建会话列表头部
276
- const sessionListHeader = document.createElement('div');
277
- sessionListHeader.className = 'opencode-session-list-header';
308
+ const sessionListHeader = document.createElement("div");
309
+ sessionListHeader.className = "opencode-session-list-header";
278
310
  sessionListHeader.innerHTML = `
279
311
  <span id="opencode-session-list-title">会话列表</span>
280
312
  <button class="opencode-new-session-btn" title="新建会话" aria-label="新建会话">+</button>
281
313
  `;
282
314
  // 创建会话列表头部骨架屏
283
- const sessionHeaderSkeleton = document.createElement('div');
284
- sessionHeaderSkeleton.className = 'opencode-session-header-skeleton';
315
+ const sessionHeaderSkeleton = document.createElement("div");
316
+ sessionHeaderSkeleton.className = "opencode-session-header-skeleton";
285
317
  sessionHeaderSkeleton.innerHTML = `
286
318
  <div class="opencode-skeleton-header-title"></div>
287
319
  <div class="opencode-skeleton-header-btn"></div>
288
320
  `;
289
321
  // 创建会话列表内容
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');
322
+ const sessionListContent = document.createElement("div");
323
+ sessionListContent.className = "opencode-session-list-content";
324
+ sessionListContent.setAttribute("role", "listbox");
325
+ sessionListContent.setAttribute("aria-labelledby", "opencode-session-list-title");
294
326
  // 创建会话列表内容骨架屏
295
- const sessionSkeleton = document.createElement('div');
296
- sessionSkeleton.className = 'opencode-session-skeleton';
327
+ const sessionSkeleton = document.createElement("div");
328
+ sessionSkeleton.className = "opencode-session-skeleton";
297
329
  sessionSkeleton.innerHTML = `
298
330
  <div class="opencode-skeleton-item">
299
331
  <div class="opencode-skeleton-title"></div>
@@ -325,96 +357,126 @@
325
357
  function toggleSessionList() {
326
358
  isSessionListCollapsed = !isSessionListCollapsed;
327
359
  if (!isSessionListCollapsed) {
328
- sessionHeaderSkeleton.classList.add('visible');
329
- sessionListHeader.style.display = 'none';
330
- sessionSkeleton.classList.add('visible');
331
- sessionListContent.style.display = 'none';
360
+ sessionHeaderSkeleton.classList.add("visible");
361
+ sessionListHeader.style.display = "none";
362
+ sessionSkeleton.classList.add("visible");
363
+ sessionListContent.style.display = "none";
332
364
  }
333
- sessionList.classList.toggle('collapsed', isSessionListCollapsed);
334
- toggleBtn.title = isSessionListCollapsed ? '展开会话列表' : '折叠会话列表';
335
- toggleBtn.setAttribute('aria-label', isSessionListCollapsed ? '展开会话列表' : '折叠会话列表');
336
- toggleBtn.setAttribute('aria-expanded', String(!isSessionListCollapsed));
365
+ sessionList.classList.toggle("collapsed", isSessionListCollapsed);
366
+ toggleBtn.title = isSessionListCollapsed
367
+ ? "展开会话列表"
368
+ : "折叠会话列表";
369
+ toggleBtn.setAttribute("aria-label", isSessionListCollapsed ? "展开会话列表" : "折叠会话列表");
370
+ toggleBtn.setAttribute("aria-expanded", String(!isSessionListCollapsed));
337
371
  if (!isSessionListCollapsed) {
338
372
  setTimeout(() => {
339
- sessionHeaderSkeleton.classList.remove('visible');
340
- sessionListHeader.style.display = '';
341
- sessionSkeleton.classList.remove('visible');
342
- sessionListContent.style.display = '';
373
+ sessionHeaderSkeleton.classList.remove("visible");
374
+ sessionListHeader.style.display = "";
375
+ sessionSkeleton.classList.remove("visible");
376
+ sessionListContent.style.display = "";
343
377
  }, 200);
344
378
  }
345
379
  }
346
- toggleBtn.addEventListener('click', toggleSessionList);
380
+ toggleBtn.addEventListener("click", toggleSessionList);
347
381
  // 创建 iframe 容器
348
- const iframeContainer = document.createElement('div');
349
- iframeContainer.className = 'opencode-iframe-container';
382
+ const iframeContainer = document.createElement("div");
383
+ iframeContainer.className = "opencode-iframe-container";
350
384
  // 创建加载指示器
351
- const loadingOverlay = document.createElement('div');
352
- loadingOverlay.className = 'opencode-loading-overlay';
385
+ const loadingOverlay = document.createElement("div");
386
+ loadingOverlay.className = "opencode-loading-overlay";
353
387
  loadingOverlay.innerHTML = `
354
388
  <div class="opencode-loading-spinner"></div>
355
389
  <div class="opencode-loading-text">加载中...</div>
356
390
  `;
391
+ // 创建空状态提示
392
+ const emptyStateOverlay = document.createElement("div");
393
+ emptyStateOverlay.className = "opencode-empty-state-overlay";
394
+ emptyStateOverlay.innerHTML = `
395
+ <div class="opencode-empty-state-icon">
396
+ <svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5">
397
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
398
+ </svg>
399
+ </div>
400
+ <div class="opencode-empty-state-text">当前项目暂无会话</div>
401
+ <button class="opencode-empty-state-btn">立即创建</button>
402
+ `;
403
+ emptyStateOverlay
404
+ .querySelector(".opencode-empty-state-btn")
405
+ .addEventListener("click", () => {
406
+ createNewSession();
407
+ });
357
408
  // 创建 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';
409
+ const iframe = document.createElement("iframe");
410
+ iframe.className = "opencode-iframe";
411
+ iframe.src = servicesStarted && sessionUrl ? sessionUrl : "about:blank";
412
+ iframe.allow = "clipboard-write; clipboard-read";
413
+ iframe.referrerPolicy = "origin";
414
+ if (servicesStarted && !sessionUrl) {
415
+ showLoading();
416
+ }
363
417
  iframe.onload = function () {
364
- if (servicesStarted) {
418
+ if (servicesStarted &&
419
+ iframe.src !== "about:blank" &&
420
+ iframe.src !== window.location.href) {
365
421
  updateContext();
366
422
  loadSessions();
367
- setupSSEConnection();
368
423
  }
369
- hideLoading();
424
+ if (iframe.src !== "about:blank" && iframe.src !== window.location.href) {
425
+ hideLoading();
426
+ }
370
427
  };
428
+ iframeContainer.appendChild(emptyStateOverlay);
371
429
  iframeContainer.appendChild(loadingOverlay);
372
430
  iframeContainer.appendChild(iframe);
373
431
  // 创建右侧工具栏
374
- const rightToolbar = document.createElement('div');
375
- rightToolbar.className = `opencode-right-toolbar${selectedElements.length === 0 ? ' collapsed' : ''}`;
432
+ const rightToolbar = document.createElement("div");
433
+ rightToolbar.className = `opencode-right-toolbar${selectedElements.length === 0 ? " collapsed" : ""}`;
376
434
  // 创建已选节点标题
377
- const selectedNodesHeader = document.createElement('div');
378
- selectedNodesHeader.className = 'opencode-selected-nodes-header';
435
+ const selectedNodesHeader = document.createElement("div");
436
+ selectedNodesHeader.className = "opencode-selected-nodes-header";
379
437
  selectedNodesHeader.innerHTML = `
380
438
  <div class="opencode-selected-nodes-title">已选节点</div>
381
439
  <div class="opencode-selected-nodes-desc">选中的节点会在对话时一起发送给助手</div>
382
440
  `;
383
441
  // 创建已选节点容器
384
- const selectedNodesContainer = document.createElement('div');
385
- selectedNodesContainer.className = 'opencode-selected-nodes';
386
- selectedNodesContainer.setAttribute('role', 'list');
387
- selectedNodesContainer.setAttribute('aria-label', '已选元素列表');
442
+ const selectedNodesContainer = document.createElement("div");
443
+ selectedNodesContainer.className = "opencode-selected-nodes";
444
+ selectedNodesContainer.setAttribute("role", "list");
445
+ selectedNodesContainer.setAttribute("aria-label", "已选元素列表");
388
446
  // 创建清空按钮
389
- const clearAllButton = document.createElement('button');
390
- clearAllButton.className = 'opencode-clear-all-btn';
391
- clearAllButton.setAttribute('aria-label', '清空所有已选节点');
392
- clearAllButton.innerHTML = '一键清空';
447
+ const clearAllButton = document.createElement("button");
448
+ clearAllButton.className = "opencode-clear-all-btn";
449
+ clearAllButton.setAttribute("aria-label", "清空所有已选节点");
450
+ clearAllButton.innerHTML = "一键清空";
393
451
  rightToolbar.appendChild(selectedNodesHeader);
394
452
  rightToolbar.appendChild(selectedNodesContainer);
395
453
  rightToolbar.appendChild(clearAllButton);
396
454
  // 创建选择模式常驻提示(固定到页面顶部)
397
- const selectModeHint = document.createElement('div');
398
- selectModeHint.className = 'opencode-select-mode-hint';
455
+ const selectModeHint = document.createElement("div");
456
+ selectModeHint.className = "opencode-select-mode-hint";
399
457
  selectModeHint.innerHTML = `
400
458
  <span>🎯 选择模式已开启 - 点击元素进行选择</span>
401
459
  <span class="opencode-hint-shortcut">按 ESC 或 Ctrl+P 退出</span>
402
460
  `;
403
461
  // 创建元素高亮覆盖层
404
- const elementHighlight = document.createElement('div');
405
- elementHighlight.className = 'opencode-element-highlight';
462
+ const elementHighlight = document.createElement("div");
463
+ elementHighlight.className = "opencode-element-highlight";
406
464
  // 创建元素信息提示框
407
- const elementTooltip = document.createElement('div');
408
- elementTooltip.className = 'opencode-element-tooltip';
465
+ const elementTooltip = document.createElement("div");
466
+ elementTooltip.className = "opencode-element-tooltip";
409
467
  // 创建已选节点气泡容器(气泡按钮上方)
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);
468
+ const selectedBubbles = document.createElement("div");
469
+ selectedBubbles.className = "opencode-selected-bubbles";
470
+ selectedBubbles.setAttribute("role", "list");
471
+ selectedBubbles.setAttribute("aria-label", "已选元素列表");
472
+ // 创建内容容器
473
+ const chatContent = document.createElement("div");
474
+ chatContent.className = "opencode-chat-content";
415
475
  chat.appendChild(chatHeader);
416
- chat.appendChild(iframeContainer);
417
- chat.appendChild(rightToolbar);
476
+ chatContent.appendChild(sessionList);
477
+ chatContent.appendChild(iframeContainer);
478
+ chatContent.appendChild(rightToolbar);
479
+ chat.appendChild(chatContent);
418
480
  container.appendChild(button);
419
481
  container.appendChild(selectedBubbles);
420
482
  container.appendChild(chat);
@@ -427,81 +489,77 @@
427
489
  }
428
490
  /** @type {Array} 会话列表 */
429
491
  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
492
  /**
444
493
  * 显示加载状态
445
494
  */
446
495
  function showLoading() {
447
- loadingOverlay.classList.add('visible');
496
+ loadingOverlay.classList.add("visible");
448
497
  }
449
498
  /**
450
499
  * 隐藏加载状态
451
500
  */
452
501
  function hideLoading() {
453
- loadingOverlay.classList.remove('visible');
502
+ loadingOverlay.classList.remove("visible");
454
503
  }
455
504
  /**
456
505
  * 加载会话列表
457
506
  */
458
507
  async function loadSessions() {
459
508
  try {
460
- const response = await fetch('/__opencode_sessions__');
509
+ const response = await fetch("/__opencode_sessions__");
461
510
  sessions = await response.json();
462
511
  renderSessionList();
463
512
  }
464
513
  catch (e) {
465
- console.error('Failed to load sessions:', e);
514
+ console.error("Failed to load sessions:", e);
466
515
  }
467
516
  }
468
517
  /**
469
518
  * 渲染会话列表
470
519
  */
471
520
  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));
521
+ sessionListContent.innerHTML = "";
522
+ const currentProjectSessions = sessions.filter((session) => session.directory === cwd);
523
+ if (currentProjectSessions.length === 0) {
524
+ emptyStateOverlay.classList.add("visible");
525
+ iframe.style.display = "none";
526
+ }
527
+ else {
528
+ emptyStateOverlay.classList.remove("visible");
529
+ iframe.style.display = "";
530
+ }
531
+ currentProjectSessions.forEach((session) => {
532
+ const item = document.createElement("div");
533
+ item.className = "opencode-session-item";
534
+ item.setAttribute("role", "option");
535
+ item.setAttribute("aria-selected", String(session.id === currentSessionId));
479
536
  if (session.id === currentSessionId) {
480
- item.classList.add('active');
537
+ item.classList.add("active");
481
538
  }
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) => {
539
+ const header = document.createElement("div");
540
+ header.className = "opencode-session-header";
541
+ const title = document.createElement("div");
542
+ title.className = "opencode-session-title";
543
+ title.textContent = session.title || "新会话";
544
+ const deleteBtn = document.createElement("button");
545
+ deleteBtn.className = "opencode-session-delete-btn";
546
+ deleteBtn.innerHTML = "×";
547
+ deleteBtn.title = "删除会话";
548
+ deleteBtn.setAttribute("aria-label", `删除会话: ${session.title || "新会话"}`);
549
+ deleteBtn.addEventListener("click", (e) => {
493
550
  e.stopPropagation();
494
551
  confirmDeleteSession(session);
495
552
  });
496
553
  header.appendChild(title);
497
554
  header.appendChild(deleteBtn);
498
- const meta = document.createElement('div');
499
- meta.className = 'opencode-session-meta';
555
+ const meta = document.createElement("div");
556
+ meta.className = "opencode-session-meta";
500
557
  const date = new Date(session.time.updated);
501
- meta.textContent = date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
558
+ meta.textContent =
559
+ date.toLocaleDateString() + " " + date.toLocaleTimeString();
502
560
  item.appendChild(header);
503
561
  item.appendChild(meta);
504
- item.addEventListener('click', () => {
562
+ item.addEventListener("click", () => {
505
563
  switchSession(session);
506
564
  });
507
565
  sessionListContent.appendChild(item);
@@ -516,7 +574,7 @@
516
574
  }
517
575
  currentSessionId = session.id;
518
576
  const encodedDir = btoa(cwd);
519
- const baseUrl = iframeUrl.split('/').slice(0, 3).join('/');
577
+ const baseUrl = webUrl;
520
578
  showLoading();
521
579
  iframe.src = `${baseUrl}/${encodedDir}/session/${session.id}`;
522
580
  renderSessionList();
@@ -526,23 +584,23 @@
526
584
  */
527
585
  async function createNewSession() {
528
586
  try {
529
- const response = await fetch('/__opencode_sessions__', {
530
- method: 'POST'
587
+ const response = await fetch("/__opencode_sessions__", {
588
+ method: "POST",
531
589
  });
532
590
  const newSession = await response.json();
533
591
  sessions.unshift(newSession);
534
592
  switchSession(newSession);
535
593
  }
536
594
  catch (e) {
537
- console.error('Failed to create session:', e);
538
- showNotification('创建会话失败');
595
+ console.error("Failed to create session:", e);
596
+ showNotification("创建会话失败");
539
597
  }
540
598
  }
541
599
  /**
542
600
  * 确认删除会话
543
601
  */
544
602
  async function confirmDeleteSession(session) {
545
- const confirmed = await showConfirmDialog(`确定要删除会话 "${session.title || '新会话'}" 吗?`);
603
+ const confirmed = await showConfirmDialog(`确定要删除会话 "${session.title || "新会话"}" 吗?`);
546
604
  if (confirmed) {
547
605
  deleteSession(session);
548
606
  }
@@ -553,60 +611,65 @@
553
611
  async function deleteSession(session) {
554
612
  try {
555
613
  const response = await fetch(`/__opencode_sessions__?id=${session.id}`, {
556
- method: 'DELETE'
614
+ method: "DELETE",
557
615
  });
558
616
  if (!response.ok) {
559
- throw new Error('Delete failed');
617
+ throw new Error("Delete failed");
560
618
  }
561
- sessions = sessions.filter(s => s.id !== session.id);
619
+ sessions = sessions.filter((s) => s.id !== session.id);
562
620
  if (session.id === currentSessionId) {
563
- const remainingSessions = sessions.filter(s => s.directory === cwd);
621
+ const remainingSessions = sessions.filter((s) => s.directory === cwd);
564
622
  if (remainingSessions.length > 0) {
565
623
  switchSession(remainingSessions[0]);
566
624
  }
567
625
  else {
568
626
  currentSessionId = null;
569
- iframe.src = 'about:blank';
627
+ iframe.src = "about:blank";
570
628
  }
571
629
  }
572
630
  renderSessionList();
573
- showNotification('会话已删除');
631
+ showNotification("会话已删除");
574
632
  }
575
633
  catch (e) {
576
- console.error('Failed to delete session:', e);
577
- showNotification('删除会话失败');
634
+ console.error("Failed to delete session:", e);
635
+ showNotification("删除会话失败");
578
636
  }
579
637
  }
580
638
  // 绑定新建会话按钮
581
- sessionListHeader.querySelector('.opencode-new-session-btn').addEventListener('click', createNewSession);
639
+ sessionListHeader
640
+ .querySelector(".opencode-new-session-btn")
641
+ .addEventListener("click", createNewSession);
582
642
  /**
583
643
  * 应用主题
584
644
  */
585
645
  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
- }
646
+ const resolvedTheme = theme === "auto"
647
+ ? window.matchMedia("(prefers-color-scheme: dark)").matches
648
+ ? "dark"
649
+ : "light"
650
+ : theme;
651
+ container.classList.toggle("opencode-dark", resolvedTheme === "dark");
652
+ container.style.colorScheme = resolvedTheme;
593
653
  }
594
654
  applyTheme();
595
- if (theme === 'auto') {
596
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme);
655
+ if (theme === "auto") {
656
+ window
657
+ .matchMedia("(prefers-color-scheme: dark)")
658
+ .addEventListener("change", applyTheme);
597
659
  }
598
660
  /**
599
661
  * 切换挂件显示状态
600
662
  */
601
663
  async function toggle() {
602
664
  if (lazy && !servicesStarted) {
603
- button.classList.add('loading');
665
+ button.classList.add("loading");
604
666
  const started = await ensureServicesStarted();
605
- button.classList.remove('loading');
667
+ button.classList.remove("loading");
606
668
  if (!started) {
607
- showNotification('服务启动失败,请检查控制台');
669
+ showNotification("服务启动失败,请检查控制台");
608
670
  return;
609
671
  }
672
+ setupSSEConnection();
610
673
  }
611
674
  if (isSelectMode) {
612
675
  exitSelectMode();
@@ -614,14 +677,14 @@
614
677
  return;
615
678
  }
616
679
  isOpen = !isOpen;
617
- chat.classList.toggle('open', isOpen);
618
- button.classList.toggle('active', isOpen);
680
+ chat.classList.toggle("open", isOpen);
681
+ button.classList.toggle("active", isOpen);
619
682
  if (isOpen) {
620
683
  updateContext();
621
684
  }
622
685
  }
623
- button.addEventListener('click', toggle);
624
- document.addEventListener('keydown', (e) => {
686
+ button.addEventListener("click", toggle);
687
+ document.addEventListener("keydown", (e) => {
625
688
  if (matchHotkey(e, mainHotkey)) {
626
689
  e.preventDefault();
627
690
  toggle();
@@ -632,7 +695,7 @@
632
695
  toggleSelectMode();
633
696
  }
634
697
  else {
635
- showNotification('Vue Inspector 未加载,无法使用元素选择功能');
698
+ showNotification("Vue Inspector 未加载,无法使用元素选择功能");
636
699
  }
637
700
  }
638
701
  });
@@ -648,14 +711,14 @@
648
711
  */
649
712
  function showConfirmDialog(message) {
650
713
  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');
714
+ const overlay = document.createElement("div");
715
+ overlay.className = "opencode-dialog-overlay";
716
+ const dialog = document.createElement("div");
717
+ dialog.className = "opencode-dialog";
718
+ dialog.setAttribute("role", "alertdialog");
719
+ dialog.setAttribute("aria-modal", "true");
720
+ dialog.setAttribute("aria-labelledby", "opencode-dialog-title");
721
+ dialog.setAttribute("aria-describedby", "opencode-dialog-desc");
659
722
  dialog.innerHTML = `
660
723
  <div class="opencode-dialog-content">
661
724
  <div id="opencode-dialog-desc" class="opencode-dialog-message">${message}</div>
@@ -673,22 +736,24 @@
673
736
  overlay.remove();
674
737
  resolve(false);
675
738
  };
676
- dialog.querySelector('.confirm').addEventListener('click', handleConfirm);
677
- dialog.querySelector('.cancel').addEventListener('click', handleCancel);
678
- overlay.addEventListener('click', (e) => {
739
+ dialog
740
+ .querySelector(".confirm")
741
+ .addEventListener("click", handleConfirm);
742
+ dialog.querySelector(".cancel").addEventListener("click", handleCancel);
743
+ overlay.addEventListener("click", (e) => {
679
744
  if (e.target === overlay) {
680
745
  handleCancel();
681
746
  }
682
747
  });
683
- document.addEventListener('keydown', function handleEscape(e) {
684
- if (e.key === 'Escape') {
685
- document.removeEventListener('keydown', handleEscape);
748
+ document.addEventListener("keydown", function handleEscape(e) {
749
+ if (e.key === "Escape") {
750
+ document.removeEventListener("keydown", handleEscape);
686
751
  handleCancel();
687
752
  }
688
753
  });
689
754
  overlay.appendChild(dialog);
690
- document.body.appendChild(overlay);
691
- dialog.querySelector('.confirm').focus();
755
+ container.appendChild(overlay);
756
+ dialog.querySelector(".confirm").focus();
692
757
  });
693
758
  }
694
759
  /**
@@ -696,8 +761,8 @@
696
761
  * @param {string} message - 通知消息
697
762
  */
698
763
  function showNotification(message) {
699
- const notification = document.createElement('div');
700
- notification.className = 'opencode-notification';
764
+ const notification = document.createElement("div");
765
+ notification.className = "opencode-notification";
701
766
  notification.textContent = message;
702
767
  chat.appendChild(notification);
703
768
  setTimeout(() => {
@@ -712,10 +777,11 @@
712
777
  const key = elementInfo.filePath && elementInfo.line
713
778
  ? `${elementInfo.filePath}:${elementInfo.line}`
714
779
  : 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
- });
780
+ const exists = key &&
781
+ selectedElements.some((el) => {
782
+ const elKey = el.filePath && el.line ? `${el.filePath}:${el.line}` : null;
783
+ return elKey === key;
784
+ });
719
785
  if (!exists) {
720
786
  selectedElements.push(elementInfo);
721
787
  saveSelectedElements(selectedElements);
@@ -726,7 +792,7 @@
726
792
  showNotification(`已选中元素 (${selectedElements.length}个)`);
727
793
  }
728
794
  else {
729
- showNotification('该元素已选中');
795
+ showNotification("该元素已选中");
730
796
  }
731
797
  }
732
798
  /**
@@ -749,17 +815,17 @@
749
815
  renderSelectedNodes();
750
816
  updateToolbarState();
751
817
  sendSelectedElements();
752
- showNotification('已清除所有选中元素');
818
+ showNotification("已清除所有选中元素");
753
819
  }
754
820
  /**
755
821
  * 更新工具栏状态
756
822
  */
757
823
  function updateToolbarState() {
758
824
  if (selectedElements.length > 0) {
759
- rightToolbar.classList.remove('collapsed');
825
+ rightToolbar.classList.remove("collapsed");
760
826
  }
761
827
  else {
762
- rightToolbar.classList.add('collapsed');
828
+ rightToolbar.classList.add("collapsed");
763
829
  }
764
830
  }
765
831
  /** @type {boolean} 是否处于元素选择模式 */
@@ -777,14 +843,14 @@
777
843
  const { targetNode, params } = inspector.getTargetNode(e);
778
844
  if (targetNode && params) {
779
845
  const rect = targetNode.getBoundingClientRect();
780
- elementHighlight.style.display = 'block';
846
+ elementHighlight.style.display = "block";
781
847
  elementHighlight.style.top = `${rect.top}px`;
782
848
  elementHighlight.style.left = `${rect.left}px`;
783
849
  elementHighlight.style.width = `${rect.width}px`;
784
850
  elementHighlight.style.height = `${rect.height}px`;
785
851
  const description = getElementDescription(targetNode);
786
- const fileName = params.file ? params.file.split('/').pop() : '';
787
- let lineInfo = '';
852
+ const fileName = params.file ? params.file.split("/").pop() : "";
853
+ let lineInfo = "";
788
854
  if (params.line) {
789
855
  lineInfo = `:${params.line}`;
790
856
  if (params.column) {
@@ -793,9 +859,9 @@
793
859
  }
794
860
  elementTooltip.innerHTML = `
795
861
  <div class="opencode-tooltip-tag">${description}</div>
796
- ${fileName ? `<div class="opencode-tooltip-file">${fileName}${lineInfo}</div>` : ''}
862
+ ${fileName ? `<div class="opencode-tooltip-file">${fileName}${lineInfo}</div>` : ""}
797
863
  `;
798
- elementTooltip.style.display = 'block';
864
+ elementTooltip.style.display = "block";
799
865
  const tooltipRect = elementTooltip.getBoundingClientRect();
800
866
  let tooltipTop = rect.top - tooltipRect.height - 8;
801
867
  let tooltipLeft = rect.left;
@@ -809,8 +875,8 @@
809
875
  elementTooltip.style.left = `${tooltipLeft}px`;
810
876
  }
811
877
  else {
812
- elementHighlight.style.display = 'none';
813
- elementTooltip.style.display = 'none';
878
+ elementHighlight.style.display = "none";
879
+ elementTooltip.style.display = "none";
814
880
  }
815
881
  }
816
882
  /**
@@ -819,34 +885,34 @@
819
885
  function toggleSelectMode() {
820
886
  const inspector = window.__VUE_INSPECTOR__;
821
887
  if (!inspector) {
822
- showNotification('Vue Inspector 未加载,无法使用元素选择功能');
888
+ showNotification("Vue Inspector 未加载,无法使用元素选择功能");
823
889
  return;
824
890
  }
825
891
  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);
892
+ selectButton.classList.toggle("active", isSelectMode);
893
+ selectButton.setAttribute("aria-pressed", String(isSelectMode));
894
+ selectModeHint.classList.toggle("visible", isSelectMode);
895
+ selectedBubbles.classList.toggle("visible", isSelectMode);
830
896
  if (isSelectMode) {
831
897
  if (isOpen) {
832
898
  isOpen = false;
833
- chat.classList.remove('open');
834
- button.classList.remove('active');
899
+ chat.classList.remove("open");
900
+ button.classList.remove("active");
835
901
  }
836
- chat.style.display = 'none';
902
+ chat.style.display = "none";
837
903
  inspector.enable();
838
904
  renderSelectedBubbles();
839
- document.addEventListener('mousemove', handleMouseMove);
905
+ document.addEventListener("mousemove", handleMouseMove);
840
906
  }
841
907
  else {
842
- chat.style.display = '';
908
+ chat.style.display = "";
843
909
  isOpen = true;
844
- chat.classList.add('open');
845
- button.classList.add('active');
910
+ chat.classList.add("open");
911
+ button.classList.add("active");
846
912
  inspector.disable();
847
- document.removeEventListener('mousemove', handleMouseMove);
848
- elementHighlight.style.display = 'none';
849
- elementTooltip.style.display = 'none';
913
+ document.removeEventListener("mousemove", handleMouseMove);
914
+ elementHighlight.style.display = "none";
915
+ elementTooltip.style.display = "none";
850
916
  }
851
917
  }
852
918
  /**
@@ -860,20 +926,20 @@
860
926
  inspector.disable();
861
927
  }
862
928
  isSelectMode = false;
863
- selectButton.classList.remove('active');
864
- selectModeHint.classList.remove('visible');
865
- selectedBubbles.classList.remove('visible');
866
- chat.style.display = '';
929
+ selectButton.classList.remove("active");
930
+ selectModeHint.classList.remove("visible");
931
+ selectedBubbles.classList.remove("visible");
932
+ chat.style.display = "";
867
933
  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';
934
+ chat.classList.add("open");
935
+ button.classList.add("active");
936
+ document.removeEventListener("mousemove", handleMouseMove);
937
+ elementHighlight.style.display = "none";
938
+ elementTooltip.style.display = "none";
873
939
  }
874
940
  // ESC 键退出选择模式(使用捕获阶段确保优先处理)
875
- document.addEventListener('keydown', (e) => {
876
- if (e.key === 'Escape' && isSelectMode) {
941
+ document.addEventListener("keydown", (e) => {
942
+ if (e.key === "Escape" && isSelectMode) {
877
943
  e.preventDefault();
878
944
  e.stopPropagation();
879
945
  exitSelectMode();
@@ -883,32 +949,41 @@
883
949
  * 建立 SSE 连接监听服务端事件
884
950
  */
885
951
  function setupSSEConnection() {
886
- if (!servicesStarted)
952
+ if (!servicesStarted || sseConnection)
887
953
  return;
888
- const eventSource = new EventSource('/__opencode_events__');
889
- eventSource.onopen = () => {
890
- console.log('[OpenCode] SSE connected');
954
+ sseConnection = new EventSource("/__opencode_events__");
955
+ sseConnection.onopen = () => {
956
+ console.log("[OpenCode] SSE connected");
891
957
  };
892
- eventSource.onmessage = (event) => {
958
+ sseConnection.onmessage = (event) => {
893
959
  try {
894
960
  const data = JSON.parse(event.data);
895
- console.log('[OpenCode] SSE message:', data);
896
- if (data.type === 'CONNECTED') {
961
+ console.log("[OpenCode] SSE message:", data);
962
+ if (data.type === "CONNECTED") {
897
963
  // 连接成功后,主动同步当前节点到服务端
898
964
  if (selectedElements.length > 0) {
899
- fetch('/__opencode_context__', {
900
- method: 'POST',
901
- headers: { 'Content-Type': 'application/json' },
965
+ fetch("/__opencode_context__", {
966
+ method: "POST",
967
+ headers: { "Content-Type": "application/json" },
902
968
  body: JSON.stringify({
903
969
  url: window.location.href,
904
970
  title: document.title,
905
- selectedElements: selectedElements
906
- })
971
+ selectedElements: selectedElements,
972
+ }),
907
973
  }).catch(() => { });
908
974
  }
909
975
  }
910
- if (data.type === 'CLEAR_ELEMENTS' && selectedElements.length > 0) {
911
- console.log('[OpenCode] Clearing elements');
976
+ if (data.type === "SESSION_READY") {
977
+ if (data.sessionUrl && !sessionUrl) {
978
+ sessionUrl = data.sessionUrl;
979
+ currentSessionId = extractSessionId(sessionUrl);
980
+ if (iframe) {
981
+ iframe.src = sessionUrl;
982
+ }
983
+ }
984
+ }
985
+ if (data.type === "CLEAR_ELEMENTS" && selectedElements.length > 0) {
986
+ console.log("[OpenCode] Clearing elements");
912
987
  selectedElements = [];
913
988
  saveSelectedElements(selectedElements);
914
989
  renderSelectedNodes();
@@ -919,13 +994,16 @@
919
994
  }
920
995
  }
921
996
  catch (e) {
922
- console.error('[OpenCode] SSE parse error:', e);
997
+ console.error("[OpenCode] SSE parse error:", e);
923
998
  }
924
999
  };
925
- eventSource.onerror = (e) => {
926
- console.error('[OpenCode] SSE error:', e);
1000
+ sseConnection.onerror = (e) => {
1001
+ console.error("[OpenCode] SSE error:", e);
927
1002
  // 连接失败时自动重连
928
- eventSource.close();
1003
+ if (sseConnection) {
1004
+ sseConnection.close();
1005
+ sseConnection = null;
1006
+ }
929
1007
  setTimeout(setupSSEConnection, 3000);
930
1008
  };
931
1009
  }
@@ -933,24 +1011,31 @@
933
1011
  * 渲染已选节点气泡
934
1012
  */
935
1013
  function renderSelectedBubbles() {
936
- selectedBubbles.innerHTML = '';
1014
+ selectedBubbles.innerHTML = "";
937
1015
  if (selectedElements.length === 0) {
938
- selectedBubbles.innerHTML = '<div class="opencode-bubble-empty">暂无选中元素</div>';
1016
+ selectedBubbles.innerHTML =
1017
+ '<div class="opencode-bubble-empty">暂无选中元素</div>';
939
1018
  return;
940
1019
  }
941
1020
  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}` : ''}` : '';
1021
+ const bubble = document.createElement("div");
1022
+ bubble.className = "opencode-selected-bubble";
1023
+ bubble.setAttribute("role", "listitem");
1024
+ const description = element.description || "未知元素";
1025
+ const fileName = element.filePath
1026
+ ? element.filePath.split("/").pop()
1027
+ : "";
1028
+ const lineInfo = element.line
1029
+ ? `:${element.line}${element.column ? `:${element.column}` : ""}`
1030
+ : "";
948
1031
  bubble.innerHTML = `
949
1032
  <span class="opencode-bubble-text">${description}</span>
950
- ${fileName ? `<span class="opencode-bubble-file">${fileName}${lineInfo}</span>` : ''}
1033
+ ${fileName ? `<span class="opencode-bubble-file">${fileName}${lineInfo}</span>` : ""}
951
1034
  <button class="opencode-bubble-remove" data-index="${index}" aria-label="移除元素: ${description}">×</button>
952
1035
  `;
953
- bubble.querySelector('.opencode-bubble-remove').addEventListener('click', (e) => {
1036
+ bubble
1037
+ .querySelector(".opencode-bubble-remove")
1038
+ .addEventListener("click", (e) => {
954
1039
  e.stopPropagation();
955
1040
  removeElement(index);
956
1041
  renderSelectedBubbles();
@@ -959,42 +1044,50 @@
959
1044
  });
960
1045
  }
961
1046
  function renderSelectedNodes() {
962
- selectedNodesContainer.innerHTML = '';
1047
+ selectedNodesContainer.innerHTML = "";
963
1048
  if (selectedElements.length === 0) {
964
- rightToolbar.classList.add('collapsed');
965
- clearAllButton.style.display = 'none';
1049
+ rightToolbar.classList.add("collapsed");
1050
+ clearAllButton.style.display = "none";
966
1051
  }
967
1052
  else {
968
- rightToolbar.classList.remove('collapsed');
969
- clearAllButton.style.display = 'block';
1053
+ rightToolbar.classList.remove("collapsed");
1054
+ clearAllButton.style.display = "block";
970
1055
  }
971
1056
  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}` : ''}` : '';
1057
+ const node = document.createElement("div");
1058
+ node.className = "opencode-selected-node";
1059
+ node.setAttribute("role", "listitem");
1060
+ const description = element.description || "未知元素";
1061
+ const textPreview = element.innerText
1062
+ ? element.innerText.substring(0, 30)
1063
+ : "";
1064
+ const fileName = element.filePath
1065
+ ? element.filePath.split("/").pop()
1066
+ : "未知文件";
1067
+ const lineInfo = element.line
1068
+ ? `:${element.line}${element.column ? `:${element.column}` : ""}`
1069
+ : "";
979
1070
  node.innerHTML = `
980
1071
  <div class="opencode-node-content">
981
1072
  <span class="opencode-node-text">${description}</span>
982
- <span class="opencode-node-file">${textPreview ? textPreview + ' · ' : ''}${fileName}${lineInfo}</span>
1073
+ <span class="opencode-node-file">${textPreview ? textPreview + " · " : ""}${fileName}${lineInfo}</span>
983
1074
  </div>
984
1075
  <button class="opencode-node-remove" data-index="${index}" aria-label="移除元素: ${description}">×</button>
985
1076
  `;
986
- node.querySelector('.opencode-node-remove').addEventListener('click', (e) => {
1077
+ node
1078
+ .querySelector(".opencode-node-remove")
1079
+ .addEventListener("click", (e) => {
987
1080
  e.stopPropagation();
988
1081
  removeElement(index);
989
1082
  });
990
- node.addEventListener('click', () => {
1083
+ node.addEventListener("click", () => {
991
1084
  highlightElement(element);
992
1085
  });
993
1086
  selectedNodesContainer.appendChild(node);
994
1087
  });
995
1088
  }
996
1089
  // 绑定清空按钮点击事件
997
- clearAllButton.addEventListener('click', async () => {
1090
+ clearAllButton.addEventListener("click", async () => {
998
1091
  if (selectedElements.length === 0)
999
1092
  return;
1000
1093
  const confirmed = await showConfirmDialog(`确定要清空所有 ${selectedElements.length} 个已选节点吗?`);
@@ -1003,7 +1096,7 @@
1003
1096
  }
1004
1097
  });
1005
1098
  // 绑定选择按钮点击事件
1006
- selectButton.addEventListener('click', toggleSelectMode);
1099
+ selectButton.addEventListener("click", toggleSelectMode);
1007
1100
  /**
1008
1101
  * 高亮页面元素
1009
1102
  * @param {SelectedElement} element - 元素信息
@@ -1014,18 +1107,18 @@
1014
1107
  if (!description)
1015
1108
  return;
1016
1109
  let targetElement = null;
1017
- if (description.includes('#')) {
1110
+ if (description.includes("#")) {
1018
1111
  const idMatch = description.match(/#([^\.\[\s]+)/);
1019
1112
  if (idMatch) {
1020
1113
  targetElement = document.getElementById(idMatch[1]);
1021
1114
  }
1022
1115
  }
1023
- if (!targetElement && description.includes('.')) {
1116
+ if (!targetElement && description.includes(".")) {
1024
1117
  const classMatch = description.match(/^([a-z]+)\.([^\[\s]+)/i);
1025
1118
  if (classMatch) {
1026
1119
  const tagName = classMatch[1];
1027
- const classes = classMatch[2].split('.').filter(Boolean);
1028
- const selector = `${tagName}.${classes.join('.')}`;
1120
+ const classes = classMatch[2].split(".").filter(Boolean);
1121
+ const selector = `${tagName}.${classes.join(".")}`;
1029
1122
  targetElement = document.querySelector(selector);
1030
1123
  }
1031
1124
  }
@@ -1037,9 +1130,9 @@
1037
1130
  }
1038
1131
  }
1039
1132
  if (targetElement) {
1040
- targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
1041
- const highlightOverlay = document.createElement('div');
1042
- highlightOverlay.className = 'opencode-element-highlight-temp';
1133
+ targetElement.scrollIntoView({ behavior: "smooth", block: "center" });
1134
+ const highlightOverlay = document.createElement("div");
1135
+ highlightOverlay.className = "opencode-element-highlight-temp";
1043
1136
  const rect = targetElement.getBoundingClientRect();
1044
1137
  highlightOverlay.style.top = `${rect.top + window.scrollY}px`;
1045
1138
  highlightOverlay.style.left = `${rect.left + window.scrollX}px`;
@@ -1052,7 +1145,7 @@
1052
1145
  }
1053
1146
  }
1054
1147
  catch (e) {
1055
- console.error('Failed to highlight element:', e);
1148
+ console.error("Failed to highlight element:", e);
1056
1149
  }
1057
1150
  }
1058
1151
  /**
@@ -1060,22 +1153,24 @@
1060
1153
  */
1061
1154
  function sendSelectedElements() {
1062
1155
  if (!servicesStarted) {
1063
- console.log('[OpenCode] sendSelectedElements: services not started');
1156
+ console.log("[OpenCode] sendSelectedElements: services not started");
1064
1157
  return;
1065
1158
  }
1066
- console.log('[OpenCode] Sending selected elements:', selectedElements);
1067
- fetch('/__opencode_context__', {
1068
- method: 'POST',
1069
- headers: { 'Content-Type': 'application/json' },
1159
+ console.log("[OpenCode] Sending selected elements:", selectedElements);
1160
+ fetch("/__opencode_context__", {
1161
+ method: "POST",
1162
+ headers: { "Content-Type": "application/json" },
1070
1163
  body: JSON.stringify({
1071
1164
  url: currentPageUrl,
1072
1165
  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);
1166
+ selectedElements: selectedElements,
1167
+ }),
1168
+ })
1169
+ .then(() => {
1170
+ console.log("[OpenCode] Selected elements sent successfully");
1171
+ })
1172
+ .catch((e) => {
1173
+ console.error("[OpenCode] Failed to send selected elements:", e);
1079
1174
  });
1080
1175
  }
1081
1176
  /**
@@ -1086,8 +1181,8 @@
1086
1181
  */
1087
1182
  function truncate(str, maxLength) {
1088
1183
  if (!str)
1089
- return '';
1090
- return str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
1184
+ return "";
1185
+ return str.length > maxLength ? str.substring(0, maxLength) + "..." : str;
1091
1186
  }
1092
1187
  /**
1093
1188
  * 获取元素的直接文本内容
@@ -1095,10 +1190,10 @@
1095
1190
  * @returns {string} 直接文本内容
1096
1191
  */
1097
1192
  function getDirectText(element) {
1098
- let text = '';
1193
+ let text = "";
1099
1194
  for (const child of element.childNodes) {
1100
1195
  if (child.nodeType === Node.TEXT_NODE) {
1101
- text += child.textContent || '';
1196
+ text += child.textContent || "";
1102
1197
  }
1103
1198
  }
1104
1199
  return text.trim();
@@ -1114,24 +1209,29 @@
1114
1209
  const id = element.id;
1115
1210
  if (id)
1116
1211
  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
- : '';
1212
+ const className = element.className && typeof element.className === "string"
1213
+ ? element.className
1214
+ .trim()
1215
+ .split(/\s+/)
1216
+ .filter(Boolean)
1217
+ .slice(0, 2)
1218
+ .join(".")
1219
+ : "";
1120
1220
  if (className)
1121
1221
  parts.push(`.${className}`);
1122
- const name = element.getAttribute('name');
1222
+ const name = element.getAttribute("name");
1123
1223
  if (name)
1124
1224
  parts.push(`[name="${name}"]`);
1125
- const placeholder = element.getAttribute('placeholder');
1225
+ const placeholder = element.getAttribute("placeholder");
1126
1226
  if (placeholder)
1127
1227
  parts.push(`[placeholder="${placeholder.substring(0, 20)}"]`);
1128
- const src = element.getAttribute('src');
1228
+ const src = element.getAttribute("src");
1129
1229
  if (src)
1130
1230
  parts.push(`[src]`);
1131
- const href = element.getAttribute('href');
1132
- if (href && href !== '#')
1231
+ const href = element.getAttribute("href");
1232
+ if (href && href !== "#")
1133
1233
  parts.push(`[href]`);
1134
- return parts.join('');
1234
+ return parts.join("");
1135
1235
  }
1136
1236
  /**
1137
1237
  * 设置 Vue Inspector 钩子
@@ -1151,7 +1251,7 @@
1151
1251
  line: params.line,
1152
1252
  column: params.column,
1153
1253
  innerText: truncate(innerText, 200),
1154
- description
1254
+ description,
1155
1255
  };
1156
1256
  addElement(elementInfo);
1157
1257
  sendSelectedElements();
@@ -1175,10 +1275,14 @@
1175
1275
  }
1176
1276
  // 导出全局 API
1177
1277
  window.OpenCodeWidget = {
1178
- open: () => { if (!isOpen)
1179
- toggle(); },
1180
- close: () => { if (isOpen)
1181
- toggle(); },
1278
+ open: () => {
1279
+ if (!isOpen)
1280
+ toggle();
1281
+ },
1282
+ close: () => {
1283
+ if (isOpen)
1284
+ toggle();
1285
+ },
1182
1286
  toggle,
1183
1287
  showNotification,
1184
1288
  updateContext,
@@ -1284,6 +1388,13 @@
1284
1388
  transform: translateY(20px) scale(0.95);
1285
1389
  transition: all 0.3s ease;
1286
1390
  display: flex;
1391
+ flex-direction: column;
1392
+ }
1393
+
1394
+ .opencode-chat-content {
1395
+ display: flex;
1396
+ flex: 1;
1397
+ overflow: hidden;
1287
1398
  }
1288
1399
 
1289
1400
  .opencode-widget.bottom-right .opencode-chat {
@@ -1313,17 +1424,15 @@
1313
1424
  }
1314
1425
 
1315
1426
  .opencode-chat-header {
1316
- position: absolute;
1317
- top: 0;
1318
- left: 0;
1319
- right: 0;
1427
+ position: relative;
1428
+ flex-shrink: 0;
1320
1429
  display: flex;
1321
1430
  align-items: center;
1322
1431
  justify-content: space-between;
1323
1432
  padding: 0 12px;
1324
1433
  height: 40px;
1325
1434
  background: #f8f9fa;
1326
- border-bottom: 1px solid #e5e7eb;
1435
+ border-bottom: 1px solid #282828;
1327
1436
  z-index: 5;
1328
1437
  }
1329
1438
 
@@ -1336,7 +1445,7 @@
1336
1445
  .opencode-chat-header-title {
1337
1446
  font-size: 14px;
1338
1447
  font-weight: 600;
1339
- color: #374151;
1448
+ color: #282828;
1340
1449
  position: absolute;
1341
1450
  left: 50%;
1342
1451
  transform: translateX(-50%);
@@ -1362,8 +1471,8 @@
1362
1471
  }
1363
1472
 
1364
1473
  .opencode-header-btn:hover {
1365
- background: #e5e7eb;
1366
- color: #374151;
1474
+ background: #282828;
1475
+ color: #282828;
1367
1476
  }
1368
1477
 
1369
1478
  .opencode-header-btn.close:hover {
@@ -1377,8 +1486,8 @@
1377
1486
  }
1378
1487
 
1379
1488
  .opencode-dark .opencode-chat-header {
1380
- background: #111827;
1381
- border-bottom-color: #374151;
1489
+ background: #121212;
1490
+ border-bottom-color: #282828;
1382
1491
  }
1383
1492
 
1384
1493
  .opencode-dark .opencode-chat-header-title {
@@ -1390,7 +1499,7 @@
1390
1499
  }
1391
1500
 
1392
1501
  .opencode-dark .opencode-header-btn:hover {
1393
- background: #374151;
1502
+ background: #282828;
1394
1503
  color: #f3f4f6;
1395
1504
  }
1396
1505
 
@@ -1400,10 +1509,9 @@
1400
1509
  }
1401
1510
 
1402
1511
  .opencode-session-list {
1403
- margin-top: 40px;
1404
1512
  width: 240px;
1405
1513
  background: #f8f9fa;
1406
- border-right: 1px solid #e5e7eb;
1514
+ border-right: 1px solid #282828;
1407
1515
  display: flex;
1408
1516
  flex-direction: column;
1409
1517
  flex-shrink: 0;
@@ -1422,13 +1530,13 @@
1422
1530
 
1423
1531
  .opencode-session-list-header {
1424
1532
  padding: 16px;
1425
- border-bottom: 1px solid #e5e7eb;
1533
+ border-bottom: 1px solid #282828;
1426
1534
  display: flex;
1427
1535
  justify-content: space-between;
1428
1536
  align-items: center;
1429
1537
  font-weight: 600;
1430
1538
  font-size: 14px;
1431
- color: #374151;
1539
+ color: #282828;
1432
1540
  }
1433
1541
 
1434
1542
  .opencode-new-session-btn {
@@ -1466,7 +1574,7 @@
1466
1574
  }
1467
1575
 
1468
1576
  .opencode-session-item:hover {
1469
- background: #e5e7eb;
1577
+ background: #282828;
1470
1578
  }
1471
1579
 
1472
1580
  .opencode-session-item.active {
@@ -1532,7 +1640,7 @@
1532
1640
 
1533
1641
  .opencode-session-header-skeleton {
1534
1642
  padding: 16px;
1535
- border-bottom: 1px solid #e5e7eb;
1643
+ border-bottom: 1px solid #282828;
1536
1644
  display: none;
1537
1645
  justify-content: space-between;
1538
1646
  align-items: center;
@@ -1545,7 +1653,7 @@
1545
1653
  .opencode-skeleton-header-title {
1546
1654
  height: 18px;
1547
1655
  width: 80px;
1548
- background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
1656
+ background: #151515;
1549
1657
  background-size: 200% 100%;
1550
1658
  animation: skeleton-loading 1.5s ease-in-out infinite;
1551
1659
  border-radius: 4px;
@@ -1554,7 +1662,7 @@
1554
1662
  .opencode-skeleton-header-btn {
1555
1663
  width: 28px;
1556
1664
  height: 28px;
1557
- background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
1665
+ background: #151515;
1558
1666
  background-size: 200% 100%;
1559
1667
  animation: skeleton-loading 1.5s ease-in-out infinite;
1560
1668
  border-radius: 6px;
@@ -1580,7 +1688,7 @@
1580
1688
 
1581
1689
  .opencode-skeleton-title {
1582
1690
  height: 16px;
1583
- background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
1691
+ background: #151515;
1584
1692
  background-size: 200% 100%;
1585
1693
  animation: skeleton-loading 1.5s ease-in-out infinite;
1586
1694
  border-radius: 4px;
@@ -1590,7 +1698,7 @@
1590
1698
 
1591
1699
  .opencode-skeleton-meta {
1592
1700
  height: 12px;
1593
- background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
1701
+ background: #151515;
1594
1702
  background-size: 200% 100%;
1595
1703
  animation: skeleton-loading 1.5s ease-in-out infinite;
1596
1704
  border-radius: 4px;
@@ -1607,22 +1715,22 @@
1607
1715
  }
1608
1716
 
1609
1717
  .opencode-dark .opencode-skeleton-item {
1610
- background: #1f2937;
1718
+ background: #1e1e1e;
1611
1719
  }
1612
1720
 
1613
1721
  .opencode-dark .opencode-skeleton-title,
1614
1722
  .opencode-dark .opencode-skeleton-meta {
1615
- background: linear-gradient(90deg, #374151 25%, #4b5563 50%, #374151 75%);
1723
+ background: linear-gradient(90deg, #282828 25%, #4b5563 50%, #282828 75%);
1616
1724
  background-size: 200% 100%;
1617
1725
  }
1618
1726
 
1619
1727
  .opencode-dark .opencode-session-header-skeleton {
1620
- border-bottom-color: #374151;
1728
+ border-bottom-color: #282828;
1621
1729
  }
1622
1730
 
1623
1731
  .opencode-dark .opencode-skeleton-header-title,
1624
1732
  .opencode-dark .opencode-skeleton-header-btn {
1625
- background: linear-gradient(90deg, #374151 25%, #4b5563 50%, #374151 75%);
1733
+ background: linear-gradient(90deg, #282828 25%, #4b5563 50%, #282828 75%);
1626
1734
  background-size: 200% 100%;
1627
1735
  }
1628
1736
 
@@ -1633,7 +1741,6 @@
1633
1741
  display: flex;
1634
1742
  flex-direction: column;
1635
1743
  margin-top: -42px;
1636
- padding-top: 40px;
1637
1744
  }
1638
1745
 
1639
1746
  .opencode-loading-overlay {
@@ -1658,7 +1765,7 @@
1658
1765
  .opencode-loading-spinner {
1659
1766
  width: 40px;
1660
1767
  height: 40px;
1661
- border: 3px solid #e5e7eb;
1768
+ border: 3px solid #282828;
1662
1769
  border-top-color: #3b82f6;
1663
1770
  border-radius: 50%;
1664
1771
  animation: spin 0.8s linear infinite;
@@ -1674,6 +1781,61 @@
1674
1781
  color: #6b7280;
1675
1782
  }
1676
1783
 
1784
+ .opencode-empty-state-overlay {
1785
+ position: absolute;
1786
+ top: 0;
1787
+ left: 0;
1788
+ right: 0;
1789
+ bottom: 0;
1790
+ background: #f8f9fa;
1791
+ display: none;
1792
+ flex-direction: column;
1793
+ align-items: center;
1794
+ justify-content: center;
1795
+ z-index: 5;
1796
+ transition: opacity 0.3s ease;
1797
+ margin-top: 42px;
1798
+ }
1799
+
1800
+ .opencode-empty-state-overlay.visible {
1801
+ display: flex;
1802
+ }
1803
+
1804
+ .opencode-empty-state-icon {
1805
+ color: #9ca3af;
1806
+ margin-bottom: 16px;
1807
+ }
1808
+
1809
+ .opencode-empty-state-text {
1810
+ color: #4b5563;
1811
+ font-size: 16px;
1812
+ font-weight: 500;
1813
+ margin-bottom: 24px;
1814
+ }
1815
+
1816
+ .opencode-empty-state-btn {
1817
+ padding: 10px 24px;
1818
+ border-radius: 8px;
1819
+ border: none;
1820
+ background: #3b82f6;
1821
+ color: white;
1822
+ font-size: 14px;
1823
+ font-weight: 500;
1824
+ cursor: pointer;
1825
+ transition: all 0.2s;
1826
+ box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
1827
+ }
1828
+
1829
+ .opencode-empty-state-btn:hover {
1830
+ background: #2563eb;
1831
+ transform: translateY(-1px);
1832
+ box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
1833
+ }
1834
+
1835
+ .opencode-empty-state-btn:active {
1836
+ transform: translateY(0);
1837
+ }
1838
+
1677
1839
  .opencode-iframe {
1678
1840
  width: 100%;
1679
1841
  height: 100%;
@@ -1685,31 +1847,36 @@
1685
1847
  }
1686
1848
 
1687
1849
  .opencode-dark .opencode-session-list {
1688
- background: #111827;
1689
- border-right-color: #374151;
1850
+ background: #121212;
1851
+ border-right-color: #282828;
1690
1852
  }
1691
1853
 
1692
1854
  .opencode-dark .opencode-session-toggle {
1693
1855
  color: #9ca3af;
1694
- border-bottom-color: #374151;
1856
+ border-bottom-color: #282828;
1695
1857
  }
1696
1858
 
1697
1859
  .opencode-dark .opencode-session-toggle:hover {
1698
- background: #374151;
1860
+ background: #282828;
1699
1861
  color: #f3f4f6;
1700
1862
  }
1701
1863
 
1702
1864
  .opencode-dark .opencode-session-list-header {
1703
- border-bottom-color: #374151;
1865
+ border-bottom-color: #282828;
1704
1866
  color: #f3f4f6;
1705
1867
  }
1706
1868
 
1707
1869
  .opencode-dark .opencode-session-item:hover {
1708
- background: #374151;
1870
+ background: #282828;
1871
+ }
1872
+
1873
+ .opencode-dark .opencode-session-item {
1874
+ color: #d1d5db;
1709
1875
  }
1710
1876
 
1711
1877
  .opencode-dark .opencode-session-item.active {
1712
1878
  background: #3b82f6;
1879
+ color: white;
1713
1880
  }
1714
1881
 
1715
1882
  .opencode-dark .opencode-loading-overlay {
@@ -1717,7 +1884,7 @@
1717
1884
  }
1718
1885
 
1719
1886
  .opencode-dark .opencode-loading-spinner {
1720
- border-color: #374151;
1887
+ border-color: #282828;
1721
1888
  border-top-color: #3b82f6;
1722
1889
  }
1723
1890
 
@@ -1725,15 +1892,26 @@
1725
1892
  color: #9ca3af;
1726
1893
  }
1727
1894
 
1895
+ .opencode-dark .opencode-empty-state-overlay {
1896
+ background: #1a1a1a;
1897
+ }
1898
+
1899
+ .opencode-dark .opencode-empty-state-icon {
1900
+ color: #4b5563;
1901
+ }
1902
+
1903
+ .opencode-dark .opencode-empty-state-text {
1904
+ color: #d1d5db;
1905
+ }
1906
+
1728
1907
  .opencode-right-toolbar {
1729
1908
  width: 140px;
1730
1909
  background: #f8f9fa;
1731
- border-left: 1px solid #e5e7eb;
1910
+ border-left: 1px solid #282828;
1732
1911
  display: flex;
1733
1912
  flex-direction: column;
1734
1913
  flex-shrink: 0;
1735
1914
  transition: width 0.2s ease;
1736
- margin-top: 40px;
1737
1915
  overflow: hidden;
1738
1916
  }
1739
1917
 
@@ -1750,13 +1928,13 @@
1750
1928
 
1751
1929
  .opencode-selected-nodes-header {
1752
1930
  padding: 12px 8px 8px;
1753
- border-bottom: 1px solid #e5e7eb;
1931
+ border-bottom: 1px solid #282828;
1754
1932
  }
1755
1933
 
1756
1934
  .opencode-selected-nodes-title {
1757
1935
  font-size: 14px;
1758
1936
  font-weight: 600;
1759
- color: #374151;
1937
+ color: #282828;
1760
1938
  margin-bottom: 4px;
1761
1939
  }
1762
1940
 
@@ -1790,7 +1968,7 @@
1790
1968
  gap: 8px;
1791
1969
  padding: 8px 10px;
1792
1970
  background: white;
1793
- border: 1px solid #e5e7eb;
1971
+ border: 1px solid #282828;
1794
1972
  border-radius: 6px;
1795
1973
  font-size: 12px;
1796
1974
  transition: all 0.2s;
@@ -1810,7 +1988,7 @@
1810
1988
  }
1811
1989
 
1812
1990
  .opencode-node-text {
1813
- color: #374151;
1991
+ color: #282828;
1814
1992
  font-weight: 500;
1815
1993
  overflow: hidden;
1816
1994
  text-overflow: ellipsis;
@@ -1870,12 +2048,12 @@
1870
2048
  }
1871
2049
 
1872
2050
  .opencode-dark .opencode-right-toolbar {
1873
- background: #111827;
1874
- border-left-color: #374151;
2051
+ background: #121212;
2052
+ border-left-color: #282828;
1875
2053
  }
1876
2054
 
1877
2055
  .opencode-dark .opencode-selected-nodes-header {
1878
- border-bottom-color: #374151;
2056
+ border-bottom-color: #282828;
1879
2057
  }
1880
2058
 
1881
2059
  .opencode-dark .opencode-selected-nodes-title {
@@ -1891,8 +2069,8 @@
1891
2069
  }
1892
2070
 
1893
2071
  .opencode-dark .opencode-selected-node {
1894
- background: #1f2937;
1895
- border-color: #374151;
2072
+ background: #1e1e1e;
2073
+ border-color: #282828;
1896
2074
  }
1897
2075
 
1898
2076
  .opencode-dark .opencode-selected-node:hover {
@@ -1985,7 +2163,7 @@
1985
2163
 
1986
2164
  .opencode-dialog-message {
1987
2165
  font-size: 15px;
1988
- color: #374151;
2166
+ color: #282828;
1989
2167
  line-height: 1.5;
1990
2168
  }
1991
2169
 
@@ -2007,11 +2185,12 @@
2007
2185
 
2008
2186
  .opencode-dialog-btn.cancel {
2009
2187
  background: #f3f4f6;
2010
- color: #374151;
2188
+ color: #282828;
2011
2189
  }
2012
2190
 
2013
2191
  .opencode-dialog-btn.cancel:hover {
2014
- background: #e5e7eb;
2192
+ background: #282828;
2193
+ color: white;
2015
2194
  }
2016
2195
 
2017
2196
  .opencode-dialog-btn.confirm {
@@ -2024,7 +2203,7 @@
2024
2203
  }
2025
2204
 
2026
2205
  .opencode-dark .opencode-dialog {
2027
- background: #1f2937;
2206
+ background: #1e1e1e;
2028
2207
  }
2029
2208
 
2030
2209
  .opencode-dark .opencode-dialog-message {
@@ -2032,7 +2211,7 @@
2032
2211
  }
2033
2212
 
2034
2213
  .opencode-dark .opencode-dialog-btn.cancel {
2035
- background: #374151;
2214
+ background: #282828;
2036
2215
  color: #f3f4f6;
2037
2216
  }
2038
2217
 
@@ -2097,7 +2276,7 @@
2097
2276
 
2098
2277
  .opencode-element-tooltip {
2099
2278
  position: fixed;
2100
- background: #1f2937;
2279
+ background: #1e1e1e;
2101
2280
  color: white;
2102
2281
  padding: 8px 12px;
2103
2282
  border-radius: 6px;
@@ -2143,7 +2322,7 @@
2143
2322
  gap: 2px;
2144
2323
  padding: 8px 10px;
2145
2324
  background: white;
2146
- border: 1px solid #e5e7eb;
2325
+ border: 1px solid #282828;
2147
2326
  border-radius: 8px;
2148
2327
  font-size: 12px;
2149
2328
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
@@ -2151,7 +2330,7 @@
2151
2330
  }
2152
2331
 
2153
2332
  .opencode-bubble-text {
2154
- color: #374151;
2333
+ color: #282828;
2155
2334
  font-weight: 500;
2156
2335
  overflow: hidden;
2157
2336
  text-overflow: ellipsis;
@@ -2200,8 +2379,8 @@
2200
2379
  }
2201
2380
 
2202
2381
  .opencode-dark .opencode-selected-bubble {
2203
- background: #1f2937;
2204
- border-color: #374151;
2382
+ background: #1e1e1e;
2383
+ border-color: #282828;
2205
2384
  }
2206
2385
 
2207
2386
  .opencode-dark .opencode-bubble-text {
@@ -2217,8 +2396,8 @@
2217
2396
  }
2218
2397
 
2219
2398
  .opencode-dark .opencode-bubble-empty {
2220
- background: #1f2937;
2221
- border-color: #374151;
2399
+ background: #1e1e1e;
2400
+ border-color: #282828;
2222
2401
  color: #6b7280;
2223
2402
  }
2224
2403
 
@@ -2259,14 +2438,15 @@
2259
2438
  * 自动初始化挂件
2260
2439
  */
2261
2440
  function autoInit() {
2262
- const script = document.currentScript || document.querySelector('script[data-opencode-config]');
2441
+ const script = document.currentScript ||
2442
+ document.querySelector("script[data-opencode-config]");
2263
2443
  if (script) {
2264
- const configBase64 = script.getAttribute('data-opencode-config');
2444
+ const configBase64 = script.getAttribute("data-opencode-config");
2265
2445
  if (configBase64) {
2266
2446
  try {
2267
2447
  const config = JSON.parse(atob(configBase64));
2268
- if (document.readyState === 'loading') {
2269
- document.addEventListener('DOMContentLoaded', function () {
2448
+ if (document.readyState === "loading") {
2449
+ document.addEventListener("DOMContentLoaded", function () {
2270
2450
  initOpenCodeWidget(config);
2271
2451
  });
2272
2452
  }
@@ -2275,7 +2455,7 @@
2275
2455
  }
2276
2456
  }
2277
2457
  catch (e) {
2278
- console.error('[OpenCode Widget] Failed to parse config:', e);
2458
+ console.error("[OpenCode Widget] Failed to parse config:", e);
2279
2459
  }
2280
2460
  }
2281
2461
  }