vite-plugin-opencode-assistant 1.0.12 → 1.0.14

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.
@@ -41,6 +41,24 @@ var import_vue = require("vue");
41
41
  var import_components = require("@vite-plugin-opencode-assistant/components");
42
42
  var import_style = require("@vite-plugin-opencode-assistant/components/style.css");
43
43
  var import_shared = require("@vite-plugin-opencode-assistant/shared");
44
+ function parseHotkey(hotkeyStr) {
45
+ if (!hotkeyStr) return { ctrl: true, shift: false, alt: false, key: "k" };
46
+ const parts = hotkeyStr.toLowerCase().split("+");
47
+ const key = parts.pop();
48
+ return {
49
+ ctrl: parts.includes("ctrl") || parts.includes("cmd") || parts.includes("meta"),
50
+ shift: parts.includes("shift"),
51
+ alt: parts.includes("alt"),
52
+ key: key || "k"
53
+ };
54
+ }
55
+ function matchHotkey(e, hotkeyConfig) {
56
+ const ctrlMatch = hotkeyConfig.ctrl ? e.ctrlKey || e.metaKey : !(e.ctrlKey || e.metaKey);
57
+ const shiftMatch = hotkeyConfig.shift ? e.shiftKey : !e.shiftKey;
58
+ const altMatch = hotkeyConfig.alt ? e.altKey : !e.altKey;
59
+ const keyMatch = e.key.toLowerCase() === hotkeyConfig.key.toLowerCase();
60
+ return ctrlMatch && shiftMatch && altMatch && keyMatch;
61
+ }
44
62
  function utf8ToBase64(str) {
45
63
  const bytes = new TextEncoder().encode(str);
46
64
  const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join("");
@@ -83,13 +101,40 @@ const App = {
83
101
  const sessions = (0, import_vue.ref)([]);
84
102
  const selectedElements = (0, import_vue.ref)([]);
85
103
  const widgetRef = (0, import_vue.ref)(null);
104
+ const chromeMcpFailed = (0, import_vue.ref)(false);
105
+ const currentTask = (0, import_vue.ref)("");
106
+ const serviceStatus = (0, import_vue.ref)("idle");
107
+ const loadingText = (0, import_vue.computed)(() => {
108
+ if (iframeLoading.value) return "\u52A0\u8F7D\u4E2D...";
109
+ if (!currentTask.value) return "\u52A0\u8F7D\u4E2D...";
110
+ return import_shared.SERVICE_STARTUP_TASKS[currentTask.value] || "\u52A0\u8F7D\u4E2D...";
111
+ });
112
+ const retryingWarmup = (0, import_vue.ref)(false);
113
+ const retryWarmup = () => __async(null, null, function* () {
114
+ retryingWarmup.value = true;
115
+ try {
116
+ const res = yield fetch("/__opencode_warmup__", { method: "POST" });
117
+ const data = yield res.json();
118
+ if (data.success) {
119
+ chromeMcpFailed.value = false;
120
+ serviceStatus.value = "ready";
121
+ showNotification("Chrome DevTools MCP \u8FDE\u63A5\u6210\u529F");
122
+ } else {
123
+ showNotification(data.error || "\u91CD\u8BD5\u5931\u8D25\uFF0C\u8BF7\u786E\u8BA4 Chrome \u8FDC\u7A0B\u8C03\u8BD5\u5DF2\u5F00\u542F");
124
+ }
125
+ } catch (e) {
126
+ console.error("[OpenCode] Retry warmup failed:", e);
127
+ showNotification("\u91CD\u8BD5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5");
128
+ } finally {
129
+ retryingWarmup.value = false;
130
+ }
131
+ });
86
132
  const {
87
133
  position = "bottom-right",
88
134
  theme: initialTheme = "auto",
89
135
  open: autoOpen = false,
90
- sessionUrl: initialSessionUrl = "",
136
+ // sessionUrl 不再从配置读取,完全依赖 SSE 状态同步
91
137
  proxyUrl: configProxyUrl = "",
92
- lazy = false,
93
138
  hotkey = "ctrl+k",
94
139
  cwd = ""
95
140
  } = config;
@@ -97,18 +142,16 @@ const App = {
97
142
  proxyUrl = configProxyUrl;
98
143
  }
99
144
  const theme = (0, import_vue.ref)(initialTheme);
100
- const isWaitingForSession = (0, import_vue.ref)(!initialSessionUrl);
101
- const computedLoading = (0, import_vue.computed)(() => loading.value || isWaitingForSession.value);
102
- let servicesStarted = !lazy;
145
+ const showSessionListSkeleton = (0, import_vue.computed)(() => serviceStatus.value === "starting");
146
+ const iframeLoading = (0, import_vue.ref)(false);
147
+ const computedLoading = (0, import_vue.computed)(() => {
148
+ return serviceStatus.value === "starting" || iframeLoading.value;
149
+ });
103
150
  const extractSessionId = (url) => {
104
151
  if (!url) return null;
105
152
  const match = url.match(/\/session\/([^/?]+)/);
106
153
  return match ? match[1] : null;
107
154
  };
108
- currentSessionId.value = extractSessionId(initialSessionUrl);
109
- if (servicesStarted && initialSessionUrl) {
110
- iframeSrc.value = toProxyUrl(initialSessionUrl);
111
- }
112
155
  try {
113
156
  const stored = sessionStorage.getItem("__opencode_selected_elements__");
114
157
  if (stored) {
@@ -182,38 +225,96 @@ const App = {
182
225
  const selectSession = (session) => {
183
226
  if (currentSessionId.value === session.id) return;
184
227
  currentSessionId.value = session.id;
185
- loading.value = true;
228
+ iframeLoading.value = true;
186
229
  iframeSrc.value = `${proxyUrl}/${utf8ToBase64(cwd)}/session/${session.id}`;
187
- setTimeout(() => {
188
- loading.value = false;
189
- }, 500);
190
230
  };
191
231
  let sseConnection = null;
232
+ let sseRetryCount = 0;
233
+ const MAX_SSE_RETRIES = 10;
234
+ const SSE_RETRY_DELAY = 1e3;
192
235
  const setupSSE = () => {
193
- if (!servicesStarted || sseConnection) return;
194
- sseConnection = new EventSource("/__opencode_events__");
195
- sseConnection.onmessage = (event) => {
196
- try {
197
- const data = JSON.parse(event.data);
198
- if (data.type === "CONNECTED") {
199
- updateContext(true);
200
- } else if (data.type === "SESSION_READY") {
201
- if (data.sessionUrl && !iframeSrc.value) {
202
- iframeSrc.value = toProxyUrl(data.sessionUrl);
203
- currentSessionId.value = extractSessionId(data.sessionUrl);
236
+ if (sseConnection) return;
237
+ try {
238
+ sseConnection = new EventSource("/__opencode_events__");
239
+ sseConnection.onmessage = (event) => {
240
+ try {
241
+ const data = JSON.parse(event.data);
242
+ if (data.type === "CONNECTED") {
243
+ updateContext(true);
244
+ sseRetryCount = 0;
245
+ } else if (data.type === "STATUS_SYNC") {
246
+ if (data.isStarted !== void 0) {
247
+ if (data.isStarted && serviceStatus.value === "idle") {
248
+ serviceStatus.value = "starting";
249
+ }
250
+ }
251
+ if (data.task) {
252
+ currentTask.value = data.task;
253
+ if (data.task === "ready") {
254
+ serviceStatus.value = "ready";
255
+ chromeMcpFailed.value = false;
256
+ if (data.sessionUrl && !iframeSrc.value) {
257
+ iframeSrc.value = toProxyUrl(data.sessionUrl);
258
+ currentSessionId.value = extractSessionId(data.sessionUrl);
259
+ }
260
+ } else if (data.task === "chrome_mcp_failed") {
261
+ serviceStatus.value = "partial";
262
+ chromeMcpFailed.value = true;
263
+ } else if (data.task === "session_creation_failed" || data.task === "opencode_not_installed" || data.task === "web_start_timeout") {
264
+ serviceStatus.value = "failed";
265
+ } else if (serviceStatus.value === "idle") {
266
+ serviceStatus.value = "starting";
267
+ }
268
+ }
269
+ if (serviceStatus.value !== "idle") {
270
+ loadSessions();
271
+ }
272
+ } else if (data.type === "TASK_UPDATE") {
273
+ currentTask.value = data.task;
274
+ if (data.task === "ready") {
275
+ serviceStatus.value = "ready";
276
+ chromeMcpFailed.value = false;
277
+ if (data.sessionUrl && !iframeSrc.value) {
278
+ iframeSrc.value = toProxyUrl(data.sessionUrl);
279
+ currentSessionId.value = extractSessionId(data.sessionUrl);
280
+ }
281
+ } else if (data.task === "chrome_mcp_failed") {
282
+ serviceStatus.value = "partial";
283
+ chromeMcpFailed.value = true;
284
+ } else if (data.task === "session_creation_failed" || data.task === "opencode_not_installed" || data.task === "web_start_timeout") {
285
+ serviceStatus.value = "failed";
286
+ } else if (serviceStatus.value === "idle") {
287
+ serviceStatus.value = "starting";
288
+ }
289
+ } else if (data.type === "CLEAR_ELEMENTS") {
290
+ selectedElements.value = [];
204
291
  }
205
- isWaitingForSession.value = false;
206
- } else if (data.type === "CLEAR_ELEMENTS") {
207
- selectedElements.value = [];
292
+ } catch (e) {
293
+ }
294
+ };
295
+ sseConnection.onerror = () => {
296
+ sseConnection == null ? void 0 : sseConnection.close();
297
+ sseConnection = null;
298
+ if (serviceStatus.value === "ready" || serviceStatus.value === "partial") {
299
+ serviceStatus.value = "starting";
208
300
  }
209
- } catch (e) {
301
+ if (sseRetryCount < MAX_SSE_RETRIES) {
302
+ sseRetryCount++;
303
+ setTimeout(setupSSE, SSE_RETRY_DELAY * sseRetryCount);
304
+ }
305
+ };
306
+ } catch (e) {
307
+ sseConnection = null;
308
+ if (sseRetryCount < MAX_SSE_RETRIES) {
309
+ sseRetryCount++;
310
+ setTimeout(setupSSE, SSE_RETRY_DELAY * sseRetryCount);
210
311
  }
211
- };
312
+ }
212
313
  };
213
314
  let currentPageUrl = "";
214
315
  let currentPageTitle = "";
215
316
  const updateContext = (force = false) => {
216
- if (!servicesStarted) return;
317
+ if (serviceStatus.value === "idle") return;
217
318
  const newUrl = window.location.href;
218
319
  const newTitle = document.title;
219
320
  if (force || newUrl !== currentPageUrl || newTitle !== currentPageTitle) {
@@ -232,17 +333,12 @@ const App = {
232
333
  }
233
334
  };
234
335
  const ensureServicesStarted = () => __async(null, null, function* () {
235
- if (servicesStarted) return true;
336
+ if (serviceStatus.value !== "idle") return true;
236
337
  try {
237
338
  const res = yield fetch("/__opencode_start__");
238
339
  const data = yield res.json();
239
340
  if (data.success) {
240
- servicesStarted = true;
241
- if (data.sessionUrl) {
242
- iframeSrc.value = toProxyUrl(data.sessionUrl);
243
- currentSessionId.value = extractSessionId(data.sessionUrl);
244
- isWaitingForSession.value = false;
245
- }
341
+ serviceStatus.value = "starting";
246
342
  setupSSE();
247
343
  return true;
248
344
  }
@@ -250,38 +346,62 @@ const App = {
250
346
  }
251
347
  return false;
252
348
  });
349
+ const mainHotkey = parseHotkey(hotkey);
350
+ const selectHotkey = parseHotkey("ctrl+p");
253
351
  (0, import_vue.onMounted)(() => {
254
- if (servicesStarted) {
352
+ if (serviceStatus.value !== "idle") {
255
353
  loadSessions();
256
354
  setupSSE();
257
355
  updateContext(true);
258
356
  }
259
- if (autoOpen && servicesStarted) {
357
+ if (autoOpen && serviceStatus.value !== "idle") {
260
358
  setTimeout(() => {
261
359
  open.value = true;
262
360
  }, 1e3);
263
361
  }
264
362
  const originalPushState = history.pushState;
265
363
  const originalReplaceState = history.replaceState;
364
+ const scheduleContextUpdate = () => {
365
+ requestAnimationFrame(() => updateContext());
366
+ };
266
367
  history.pushState = function(...args) {
267
368
  originalPushState.apply(this, args);
268
- setTimeout(updateContext, 0);
369
+ scheduleContextUpdate();
269
370
  };
270
371
  history.replaceState = function(...args) {
271
372
  originalReplaceState.apply(this, args);
272
- setTimeout(updateContext, 0);
373
+ scheduleContextUpdate();
273
374
  };
274
- window.addEventListener("popstate", () => setTimeout(updateContext, 0));
275
- window.addEventListener("hashchange", () => setTimeout(updateContext, 0));
375
+ window.addEventListener("popstate", scheduleContextUpdate);
376
+ window.addEventListener("hashchange", scheduleContextUpdate);
276
377
  const titleObserver = new MutationObserver(() => {
277
378
  if (document.title !== currentPageTitle) updateContext();
278
379
  });
279
380
  if (document.head) {
280
381
  titleObserver.observe(document.head, { childList: true, subtree: true });
281
382
  }
383
+ const handleKeydown = (e) => {
384
+ if (matchHotkey(e, mainHotkey)) {
385
+ e.preventDefault();
386
+ handleToggle(!open.value);
387
+ }
388
+ if (matchHotkey(e, selectHotkey)) {
389
+ e.preventDefault();
390
+ const win = window;
391
+ if (win.__VUE_INSPECTOR__) {
392
+ selectMode.value = !selectMode.value;
393
+ } else {
394
+ showNotification("Vue Inspector \u672A\u52A0\u8F7D\uFF0C\u65E0\u6CD5\u4F7F\u7528\u5143\u7D20\u9009\u62E9\u529F\u80FD");
395
+ }
396
+ }
397
+ };
398
+ document.addEventListener("keydown", handleKeydown);
399
+ return () => {
400
+ document.removeEventListener("keydown", handleKeydown);
401
+ };
282
402
  });
283
403
  const handleToggle = (val) => __async(null, null, function* () {
284
- if (lazy && !servicesStarted && val) {
404
+ if (serviceStatus.value === "idle" && val) {
285
405
  loading.value = true;
286
406
  const started = yield ensureServicesStarted();
287
407
  loading.value = false;
@@ -292,6 +412,9 @@ const App = {
292
412
  }
293
413
  open.value = val;
294
414
  if (val) updateContext();
415
+ if (val) {
416
+ iframeLoading.value = false;
417
+ }
295
418
  });
296
419
  const handleSelectNode = (element) => {
297
420
  const exists = selectedElements.value.some(
@@ -311,45 +434,116 @@ const App = {
311
434
  showNotification("\u5DF2\u6E05\u9664\u6240\u6709\u9009\u4E2D\u5143\u7D20");
312
435
  };
313
436
  return () => {
314
- return (0, import_vue.h)(import_components.OpenCodeWidget, {
315
- ref: widgetRef,
316
- position,
317
- theme: theme.value,
318
- open: open.value,
319
- selectMode: selectMode.value,
320
- sessionListCollapsed: sessionListCollapsed.value,
321
- loading: computedLoading.value,
322
- loadingSessionList: loadingSessionList.value,
323
- iframeSrc: iframeSrc.value,
324
- currentSessionId: currentSessionId.value,
325
- sessions: sessions.value,
326
- sessionKey: "id",
327
- selectedElements: selectedElements.value,
328
- hotkeyLabel: hotkey,
329
- "onUpdate:open": handleToggle,
330
- "onUpdate:selectMode": (val) => {
331
- selectMode.value = val;
332
- },
333
- "onUpdate:sessionListCollapsed": (val) => {
334
- sessionListCollapsed.value = val;
335
- },
336
- "onUpdate:theme": (val) => {
337
- theme.value = val;
338
- },
339
- "onToggle-theme": (val) => {
340
- theme.value = val;
341
- },
342
- "onCreate-session": createSession,
343
- "onDelete-session": deleteSession,
344
- "onSelect-session": selectSession,
345
- "onClick-selected-node": handleSelectNode,
346
- "onClear-selected-nodes": handleClearSelected,
347
- "onRemove-selected-node": ({ index }) => {
348
- selectedElements.value.splice(index, 1);
349
- updateContext(true);
437
+ return (0, import_vue.h)(
438
+ import_components.OpenCodeWidget,
439
+ {
440
+ ref: widgetRef,
441
+ position,
442
+ theme: theme.value,
443
+ open: open.value,
444
+ selectMode: selectMode.value,
445
+ sessionListCollapsed: sessionListCollapsed.value,
446
+ frameLoading: computedLoading.value,
447
+ loadingSessionList: loadingSessionList.value,
448
+ showSessionListSkeleton: showSessionListSkeleton.value,
449
+ showError: chromeMcpFailed.value,
450
+ iframeSrc: iframeSrc.value,
451
+ currentSessionId: currentSessionId.value,
452
+ sessions: sessions.value,
453
+ sessionKey: "id",
454
+ selectedElements: selectedElements.value,
455
+ hotkeyLabel: hotkey,
456
+ "onUpdate:open": handleToggle,
457
+ "onUpdate:selectMode": (val) => {
458
+ selectMode.value = val;
459
+ if (!val && !open.value) {
460
+ open.value = true;
461
+ }
462
+ },
463
+ "onUpdate:sessionListCollapsed": (val) => {
464
+ sessionListCollapsed.value = val;
465
+ },
466
+ "onUpdate:theme": (val) => {
467
+ theme.value = val;
468
+ },
469
+ "onToggle-theme": (val) => {
470
+ theme.value = val;
471
+ },
472
+ "onCreate-session": createSession,
473
+ "onDelete-session": deleteSession,
474
+ "onSelect-session": selectSession,
475
+ "onClick-selected-node": handleSelectNode,
476
+ "onClear-selected-nodes": handleClearSelected,
477
+ "onRemove-selected-node": ({ index }) => {
478
+ selectedElements.value.splice(index, 1);
479
+ updateContext(true);
480
+ },
481
+ "onEmpty-action": createSession,
482
+ "onFrame-loaded": () => {
483
+ iframeLoading.value = false;
484
+ }
350
485
  },
351
- "onEmpty-action": createSession
352
- });
486
+ {
487
+ loading: () => (0, import_vue.h)("div", { class: "opencode-custom-loading" }, [
488
+ (0, import_vue.h)("div", { class: "opencode-loading-spinner" }),
489
+ (0, import_vue.h)("div", { class: "opencode-loading-text" }, loadingText.value)
490
+ ]),
491
+ error: () => chromeMcpFailed.value ? (0, import_vue.h)("div", { class: "opencode-chrome-warmup-failed" }, [
492
+ (0, import_vue.h)("div", { class: "opencode-chrome-warmup-failed-icon" }, [
493
+ (0, import_vue.h)(
494
+ "svg",
495
+ {
496
+ viewBox: "0 0 24 24",
497
+ width: "48",
498
+ height: "48",
499
+ fill: "none",
500
+ stroke: "currentColor",
501
+ strokeWidth: "1.5"
502
+ },
503
+ [
504
+ (0, import_vue.h)("path", {
505
+ strokeLinecap: "round",
506
+ strokeLinejoin: "round",
507
+ d: "M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
508
+ })
509
+ ]
510
+ )
511
+ ]),
512
+ (0, import_vue.h)(
513
+ "div",
514
+ { class: "opencode-chrome-warmup-failed-title" },
515
+ "Chrome DevTools MCP \u8FDE\u63A5\u5931\u8D25"
516
+ ),
517
+ (0, import_vue.h)("div", { class: "opencode-chrome-warmup-failed-text" }, [
518
+ (0, import_vue.h)("p", {}, "\u8BF7\u6309\u4EE5\u4E0B\u6B65\u9AA4\u5F00\u542F Chrome \u8FDC\u7A0B\u8C03\u8BD5\uFF1A"),
519
+ (0, import_vue.h)("ol", { class: "opencode-chrome-warmup-steps" }, [
520
+ (0, import_vue.h)("li", {}, [
521
+ "\u5728 Chrome \u5730\u5740\u680F\u8F93\u5165 ",
522
+ (0, import_vue.h)(
523
+ "code",
524
+ { class: "opencode-chrome-warmup-code" },
525
+ "chrome://inspect/#remote-debugging"
526
+ )
527
+ ]),
528
+ (0, import_vue.h)("li", {}, "\u52FE\u9009 'Allow remote debugging for this browser instance' \u9009\u9879"),
529
+ (0, import_vue.h)("li", {}, "\u91CD\u65B0\u542F\u52A8\u6D4F\u89C8\u5668"),
530
+ (0, import_vue.h)("li", {}, "\u5B8C\u6210\u540E\u70B9\u51FB\u4E0B\u65B9\u6309\u94AE\u91CD\u8BD5")
531
+ ])
532
+ ]),
533
+ (0, import_vue.h)("div", { class: "opencode-chrome-warmup-failed-actions" }, [
534
+ (0, import_vue.h)(
535
+ "button",
536
+ {
537
+ class: "opencode-chrome-warmup-failed-btn primary",
538
+ disabled: retryingWarmup.value,
539
+ onClick: retryWarmup
540
+ },
541
+ retryingWarmup.value ? "\u8FDE\u63A5\u4E2D..." : "\u91CD\u8BD5\u8FDE\u63A5"
542
+ )
543
+ ])
544
+ ]) : null
545
+ }
546
+ );
353
547
  };
354
548
  }
355
549
  };
@@ -358,5 +552,148 @@ if (!window[INIT_MARKER]) {
358
552
  window[INIT_MARKER] = true;
359
553
  const container = document.createElement("div");
360
554
  document.body.appendChild(container);
361
- (0, import_vue.createApp)(App).mount(container);
555
+ const app = (0, import_vue.createApp)(App);
556
+ app.mount(container);
557
+ window.__OPENCODE_CLEANUP__ = () => {
558
+ app.unmount();
559
+ container.remove();
560
+ window[INIT_MARKER] = false;
561
+ };
562
+ }
563
+ const style = document.createElement("style");
564
+ style.textContent = `
565
+ .opencode-custom-loading {
566
+ display: flex;
567
+ flex-direction: column;
568
+ align-items: center;
569
+ justify-content: center;
570
+ padding: 20px;
571
+ }
572
+
573
+ .opencode-loading-spinner {
574
+ width: 32px;
575
+ height: 32px;
576
+ border: 3px solid var(--oc-border);
577
+ border-top-color: var(--oc-primary);
578
+ border-radius: 50%;
579
+ animation: opencode-spin 0.8s linear infinite;
580
+ }
581
+
582
+ @keyframes opencode-spin {
583
+ to { transform: rotate(360deg); }
584
+ }
585
+
586
+ .opencode-loading-text {
587
+ margin-top: 12px;
588
+ color: var(--oc-text-secondary);
589
+ font-size: 14px;
590
+ }
591
+
592
+ .opencode-chrome-warmup-failed {
593
+ position: absolute;
594
+ top: 0;
595
+ left: 0;
596
+ right: 0;
597
+ bottom: 0;
598
+ background: var(--oc-bg-secondary);
599
+ display: flex;
600
+ flex-direction: column;
601
+ align-items: center;
602
+ justify-content: center;
603
+ z-index: 15;
604
+ }
605
+
606
+ .opencode-chrome-warmup-failed-icon {
607
+ color: var(--oc-warning, #f59e0b);
608
+ margin-bottom: 16px;
609
+ }
610
+
611
+ .opencode-chrome-warmup-failed-title {
612
+ color: var(--oc-text-primary);
613
+ font-size: 18px;
614
+ font-weight: 600;
615
+ margin-bottom: 8px;
616
+ }
617
+
618
+ .opencode-chrome-warmup-failed-text {
619
+ color: var(--oc-text-secondary);
620
+ font-size: 14px;
621
+ margin-bottom: 24px;
622
+ text-align: left;
623
+ max-width: 400px;
624
+ line-height: 1.6;
625
+ text-align: center;
626
+ }
627
+
628
+ .opencode-chrome-warmup-failed-text p {
629
+ margin: 0 0 12px 0;
630
+ font-weight: 500;
631
+ color: var(--oc-text-primary);
632
+ }
633
+
634
+ .opencode-chrome-warmup-steps {
635
+ margin: 0;
636
+ padding-left: 20px;
637
+ }
638
+
639
+ .opencode-chrome-warmup-steps li {
640
+ margin-bottom: 8px;
641
+ color: var(--oc-text-secondary);
642
+ font-size: 13px;
643
+ line-height: 1.5;
644
+ }
645
+
646
+ .opencode-chrome-warmup-steps li:last-child {
647
+ margin-bottom: 0;
648
+ }
649
+
650
+ .opencode-chrome-warmup-code {
651
+ display: inline-block;
652
+ background: var(--oc-bg-tertiary);
653
+ color: var(--oc-primary);
654
+ padding: 2px 6px;
655
+ border-radius: 4px;
656
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
657
+ font-size: 12px;
658
+ font-weight: 500;
659
+ word-break: break-all;
660
+ margin: 0 2px;
661
+ }
662
+
663
+ .opencode-chrome-warmup-failed-actions {
664
+ display: flex;
665
+ gap: 12px;
666
+ }
667
+
668
+ .opencode-chrome-warmup-failed-btn {
669
+ padding: 10px 24px;
670
+ border-radius: 8px;
671
+ border: none;
672
+ font-size: 14px;
673
+ font-weight: 500;
674
+ cursor: pointer;
675
+ transition: all 0.2s;
676
+ }
677
+
678
+ .opencode-chrome-warmup-failed-btn.primary {
679
+ background: var(--oc-primary);
680
+ color: white;
681
+ box-shadow: var(--oc-shadow-primary);
682
+ }
683
+
684
+ .opencode-chrome-warmup-failed-btn.primary:hover:not(:disabled) {
685
+ background: var(--oc-primary-hover);
686
+ transform: translateY(-1px);
687
+ box-shadow: var(--oc-shadow-primary-hover);
688
+ }
689
+
690
+ .opencode-chrome-warmup-failed-btn.primary:active:not(:disabled) {
691
+ transform: translateY(0);
692
+ }
693
+
694
+ .opencode-chrome-warmup-failed-btn:disabled {
695
+ opacity: 0.6;
696
+ cursor: not-allowed;
362
697
  }
698
+ `;
699
+ document.head.appendChild(style);