vite-plugin-opencode-assistant 1.0.13 → 1.0.15

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.
@@ -101,13 +101,40 @@ const App = {
101
101
  const sessions = (0, import_vue.ref)([]);
102
102
  const selectedElements = (0, import_vue.ref)([]);
103
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
+ });
104
132
  const {
105
133
  position = "bottom-right",
106
134
  theme: initialTheme = "auto",
107
135
  open: autoOpen = false,
108
- sessionUrl: initialSessionUrl = "",
136
+ // sessionUrl 不再从配置读取,完全依赖 SSE 状态同步
109
137
  proxyUrl: configProxyUrl = "",
110
- lazy = false,
111
138
  hotkey = "ctrl+k",
112
139
  cwd = ""
113
140
  } = config;
@@ -115,18 +142,16 @@ const App = {
115
142
  proxyUrl = configProxyUrl;
116
143
  }
117
144
  const theme = (0, import_vue.ref)(initialTheme);
118
- const isWaitingForSession = (0, import_vue.ref)(!initialSessionUrl);
119
- const computedLoading = (0, import_vue.computed)(() => loading.value || isWaitingForSession.value);
120
- 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
+ });
121
150
  const extractSessionId = (url) => {
122
151
  if (!url) return null;
123
152
  const match = url.match(/\/session\/([^/?]+)/);
124
153
  return match ? match[1] : null;
125
154
  };
126
- currentSessionId.value = extractSessionId(initialSessionUrl);
127
- if (servicesStarted && initialSessionUrl) {
128
- iframeSrc.value = toProxyUrl(initialSessionUrl);
129
- }
130
155
  try {
131
156
  const stored = sessionStorage.getItem("__opencode_selected_elements__");
132
157
  if (stored) {
@@ -200,38 +225,96 @@ const App = {
200
225
  const selectSession = (session) => {
201
226
  if (currentSessionId.value === session.id) return;
202
227
  currentSessionId.value = session.id;
203
- loading.value = true;
228
+ iframeLoading.value = true;
204
229
  iframeSrc.value = `${proxyUrl}/${utf8ToBase64(cwd)}/session/${session.id}`;
205
- setTimeout(() => {
206
- loading.value = false;
207
- }, 500);
208
230
  };
209
231
  let sseConnection = null;
232
+ let sseRetryCount = 0;
233
+ const MAX_SSE_RETRIES = 10;
234
+ const SSE_RETRY_DELAY = 1e3;
210
235
  const setupSSE = () => {
211
- if (!servicesStarted || sseConnection) return;
212
- sseConnection = new EventSource("/__opencode_events__");
213
- sseConnection.onmessage = (event) => {
214
- try {
215
- const data = JSON.parse(event.data);
216
- if (data.type === "CONNECTED") {
217
- updateContext(true);
218
- } else if (data.type === "SESSION_READY") {
219
- if (data.sessionUrl && !iframeSrc.value) {
220
- iframeSrc.value = toProxyUrl(data.sessionUrl);
221
- 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 = [];
222
291
  }
223
- isWaitingForSession.value = false;
224
- } else if (data.type === "CLEAR_ELEMENTS") {
225
- 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";
226
300
  }
227
- } 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);
228
311
  }
229
- };
312
+ }
230
313
  };
231
314
  let currentPageUrl = "";
232
315
  let currentPageTitle = "";
233
316
  const updateContext = (force = false) => {
234
- if (!servicesStarted) return;
317
+ if (serviceStatus.value === "idle") return;
235
318
  const newUrl = window.location.href;
236
319
  const newTitle = document.title;
237
320
  if (force || newUrl !== currentPageUrl || newTitle !== currentPageTitle) {
@@ -250,17 +333,12 @@ const App = {
250
333
  }
251
334
  };
252
335
  const ensureServicesStarted = () => __async(null, null, function* () {
253
- if (servicesStarted) return true;
336
+ if (serviceStatus.value !== "idle") return true;
254
337
  try {
255
338
  const res = yield fetch("/__opencode_start__");
256
339
  const data = yield res.json();
257
340
  if (data.success) {
258
- servicesStarted = true;
259
- if (data.sessionUrl) {
260
- iframeSrc.value = toProxyUrl(data.sessionUrl);
261
- currentSessionId.value = extractSessionId(data.sessionUrl);
262
- isWaitingForSession.value = false;
263
- }
341
+ serviceStatus.value = "starting";
264
342
  setupSSE();
265
343
  return true;
266
344
  }
@@ -271,28 +349,31 @@ const App = {
271
349
  const mainHotkey = parseHotkey(hotkey);
272
350
  const selectHotkey = parseHotkey("ctrl+p");
273
351
  (0, import_vue.onMounted)(() => {
274
- if (servicesStarted) {
352
+ if (serviceStatus.value !== "idle") {
275
353
  loadSessions();
276
354
  setupSSE();
277
355
  updateContext(true);
278
356
  }
279
- if (autoOpen && servicesStarted) {
357
+ if (autoOpen && serviceStatus.value !== "idle") {
280
358
  setTimeout(() => {
281
359
  open.value = true;
282
360
  }, 1e3);
283
361
  }
284
362
  const originalPushState = history.pushState;
285
363
  const originalReplaceState = history.replaceState;
364
+ const scheduleContextUpdate = () => {
365
+ requestAnimationFrame(() => updateContext());
366
+ };
286
367
  history.pushState = function(...args) {
287
368
  originalPushState.apply(this, args);
288
- setTimeout(updateContext, 0);
369
+ scheduleContextUpdate();
289
370
  };
290
371
  history.replaceState = function(...args) {
291
372
  originalReplaceState.apply(this, args);
292
- setTimeout(updateContext, 0);
373
+ scheduleContextUpdate();
293
374
  };
294
- window.addEventListener("popstate", () => setTimeout(updateContext, 0));
295
- window.addEventListener("hashchange", () => setTimeout(updateContext, 0));
375
+ window.addEventListener("popstate", scheduleContextUpdate);
376
+ window.addEventListener("hashchange", scheduleContextUpdate);
296
377
  const titleObserver = new MutationObserver(() => {
297
378
  if (document.title !== currentPageTitle) updateContext();
298
379
  });
@@ -320,7 +401,7 @@ const App = {
320
401
  };
321
402
  });
322
403
  const handleToggle = (val) => __async(null, null, function* () {
323
- if (lazy && !servicesStarted && val) {
404
+ if (serviceStatus.value === "idle" && val) {
324
405
  loading.value = true;
325
406
  const started = yield ensureServicesStarted();
326
407
  loading.value = false;
@@ -331,6 +412,9 @@ const App = {
331
412
  }
332
413
  open.value = val;
333
414
  if (val) updateContext();
415
+ if (val) {
416
+ iframeLoading.value = false;
417
+ }
334
418
  });
335
419
  const handleSelectNode = (element) => {
336
420
  const exists = selectedElements.value.some(
@@ -350,48 +434,116 @@ const App = {
350
434
  showNotification("\u5DF2\u6E05\u9664\u6240\u6709\u9009\u4E2D\u5143\u7D20");
351
435
  };
352
436
  return () => {
353
- return (0, import_vue.h)(import_components.OpenCodeWidget, {
354
- ref: widgetRef,
355
- position,
356
- theme: theme.value,
357
- open: open.value,
358
- selectMode: selectMode.value,
359
- sessionListCollapsed: sessionListCollapsed.value,
360
- loading: computedLoading.value,
361
- loadingSessionList: loadingSessionList.value,
362
- iframeSrc: iframeSrc.value,
363
- currentSessionId: currentSessionId.value,
364
- sessions: sessions.value,
365
- sessionKey: "id",
366
- selectedElements: selectedElements.value,
367
- hotkeyLabel: hotkey,
368
- "onUpdate:open": handleToggle,
369
- "onUpdate:selectMode": (val) => {
370
- selectMode.value = val;
371
- if (!val && !open.value) {
372
- open.value = 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;
373
484
  }
374
485
  },
375
- "onUpdate:sessionListCollapsed": (val) => {
376
- sessionListCollapsed.value = val;
377
- },
378
- "onUpdate:theme": (val) => {
379
- theme.value = val;
380
- },
381
- "onToggle-theme": (val) => {
382
- theme.value = val;
383
- },
384
- "onCreate-session": createSession,
385
- "onDelete-session": deleteSession,
386
- "onSelect-session": selectSession,
387
- "onClick-selected-node": handleSelectNode,
388
- "onClear-selected-nodes": handleClearSelected,
389
- "onRemove-selected-node": ({ index }) => {
390
- selectedElements.value.splice(index, 1);
391
- updateContext(true);
392
- },
393
- "onEmpty-action": createSession
394
- });
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
+ );
395
547
  };
396
548
  }
397
549
  };
@@ -400,5 +552,148 @@ if (!window[INIT_MARKER]) {
400
552
  window[INIT_MARKER] = true;
401
553
  const container = document.createElement("div");
402
554
  document.body.appendChild(container);
403
- (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;
404
697
  }
698
+ `;
699
+ document.head.appendChild(style);