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.
package/lib/core/api.d.ts CHANGED
@@ -11,4 +11,5 @@ export declare class OpenCodeAPI {
11
11
  getToolIds(retries?: number): Promise<string[]>;
12
12
  warmupChromeMcp(viteOrigin?: string): Promise<void>;
13
13
  getOrCreateSession(): Promise<string>;
14
+ retryWarmupChromeMcp(viteOrigin?: string): Promise<boolean>;
14
15
  }
package/lib/core/api.js CHANGED
@@ -61,13 +61,36 @@ function sleep(ms) {
61
61
  function base64Encode(str) {
62
62
  return Buffer.from(str).toString("base64");
63
63
  }
64
+ function extractTextFromResponse(data) {
65
+ if (!data || typeof data !== "object") return null;
66
+ const obj = data;
67
+ if (obj.parts && Array.isArray(obj.parts)) {
68
+ const textParts = obj.parts.filter(
69
+ (p) => p && typeof p === "object" && p.type === "text"
70
+ ).map((p) => p.text).filter(Boolean);
71
+ if (textParts.length > 0) return textParts.join("");
72
+ }
73
+ if (obj.text && typeof obj.text === "string") {
74
+ return obj.text;
75
+ }
76
+ if (obj.content && typeof obj.content === "string") {
77
+ return obj.content;
78
+ }
79
+ if (obj.message && typeof obj.message === "string") {
80
+ return obj.message;
81
+ }
82
+ if (typeof data === "string") {
83
+ return data;
84
+ }
85
+ return null;
86
+ }
64
87
  class OpenCodeAPI {
65
88
  constructor(hostname, getPort, warmupChromeMcpConfig = false) {
66
89
  __publicField(this, "hostname", hostname);
67
90
  __publicField(this, "getPort", getPort);
68
91
  __publicField(this, "warmupChromeMcpConfig", warmupChromeMcpConfig);
69
92
  }
70
- createHttpRequest(options, body) {
93
+ createHttpRequest(options, body, timeout) {
71
94
  const timer = new import_shared.PerformanceTimer("HTTP Request", {
72
95
  operation: `${options.method || "GET"} ${options.path}`
73
96
  });
@@ -90,6 +113,13 @@ class OpenCodeAPI {
90
113
  timer.end("\u274C Request failed");
91
114
  reject(e);
92
115
  });
116
+ if (timeout) {
117
+ req.setTimeout(timeout, () => {
118
+ timer.end("\u274C Request timeout");
119
+ req.destroy();
120
+ reject(new Error(`Request timeout after ${timeout}ms`));
121
+ });
122
+ }
93
123
  if (body) req.write(body);
94
124
  req.end();
95
125
  });
@@ -243,24 +273,16 @@ class OpenCodeAPI {
243
273
  try {
244
274
  const warmupSession = yield this.createSession(import_shared.DEFAULT_RETRIES, "__chrome_mcp_warmup__");
245
275
  warmupSessionId = warmupSession.id;
246
- let chromeToolIds;
247
- try {
248
- const toolIds = yield this.getToolIds();
249
- chromeToolIds = toolIds.filter((toolId) => /chrome[-_]?devtools/i.test(toolId));
250
- log.debug("Resolved Chrome MCP tool ids", {
251
- chromeToolIds
252
- });
253
- } catch (e) {
254
- log.debug("Failed to resolve Chrome MCP tool ids", { error: e });
255
- }
256
276
  const prompt = [
257
277
  "Call the browser tool list_pages immediately to establish the Chrome DevTools MCP connection.",
258
278
  viteOrigin ? `If there are no pages, call new_page with ${viteOrigin}.` : "If there are no pages, call new_page with about:blank.",
259
279
  "Do not read or modify project files.",
260
280
  "Do not use any non-browser tools.",
261
- "After the tool call is complete, reply with exactly: ready"
281
+ "After the tool call is complete, reply with exactly: ready",
282
+ "If the tool call fails, reply with exactly: fail"
262
283
  ].join(" ");
263
- yield this.createHttpRequest(
284
+ const WARMUP_TIMEOUT = 3e4;
285
+ const data = yield this.createHttpRequest(
264
286
  {
265
287
  hostname: this.hostname,
266
288
  port: this.getPort(),
@@ -270,14 +292,19 @@ class OpenCodeAPI {
270
292
  },
271
293
  JSON.stringify({
272
294
  system: "You are warming up Chrome DevTools MCP during startup. You must use the available browser tools immediately before replying.",
273
- tools: (chromeToolIds == null ? void 0 : chromeToolIds.length) ? chromeToolIds : void 0,
274
295
  parts: [{ type: "text", text: prompt }]
275
- })
296
+ }),
297
+ WARMUP_TIMEOUT
276
298
  );
299
+ const responseText = extractTextFromResponse(data);
300
+ if (!(responseText == null ? void 0 : responseText.toLowerCase().includes("ready"))) {
301
+ throw new Error(`Chrome MCP warmup failed: ${responseText || "No response"}`);
302
+ }
277
303
  timer.end("Chrome MCP warmed up");
278
304
  } catch (e) {
279
305
  log.warn("Failed to warm up Chrome MCP", { error: e });
280
306
  timer.end("Chrome MCP warmup skipped");
307
+ throw e;
281
308
  } finally {
282
309
  if (warmupSessionId) {
283
310
  try {
@@ -314,6 +341,61 @@ class OpenCodeAPI {
314
341
  return url;
315
342
  });
316
343
  }
344
+ retryWarmupChromeMcp(viteOrigin) {
345
+ return __async(this, null, function* () {
346
+ const timer = log.timer("retryWarmupChromeMcp", { viteOrigin });
347
+ let warmupSessionId = null;
348
+ try {
349
+ const warmupSession = yield this.createSession(import_shared.DEFAULT_RETRIES, "__chrome_mcp_warmup__");
350
+ warmupSessionId = warmupSession.id;
351
+ const prompt = [
352
+ "Call the browser tool list_pages immediately to establish the Chrome DevTools MCP connection.",
353
+ viteOrigin ? `If there are no pages, call new_page with ${viteOrigin}.` : "If there are no pages, call new_page with about:blank.",
354
+ "Do not read or modify project files.",
355
+ "Do not use any non-browser tools.",
356
+ "After the tool call is complete, reply with exactly: ready",
357
+ "If the tool call fails, reply with exactly: fail"
358
+ ].join(" ");
359
+ const WARMUP_TIMEOUT = 6e4;
360
+ const data = yield this.createHttpRequest(
361
+ {
362
+ hostname: this.hostname,
363
+ port: this.getPort(),
364
+ path: `/session/${warmupSessionId}/message`,
365
+ method: "POST",
366
+ headers: { "Content-Type": "application/json" }
367
+ },
368
+ JSON.stringify({
369
+ system: "You are warming up Chrome DevTools MCP during startup. You must use the available browser tools immediately before replying.",
370
+ parts: [{ type: "text", text: prompt }]
371
+ }),
372
+ WARMUP_TIMEOUT
373
+ );
374
+ log.debug("Chrome MCP warmup response:", { data });
375
+ const responseText = extractTextFromResponse(data);
376
+ if (!(responseText == null ? void 0 : responseText.toLowerCase().includes("ready"))) {
377
+ throw new Error(`Chrome MCP warmup failed: ${responseText || "No response"}`);
378
+ }
379
+ timer.end("Chrome MCP warmed up successfully");
380
+ return true;
381
+ } catch (e) {
382
+ log.warn("Failed to retry warm up Chrome MCP", { error: e });
383
+ timer.end("Chrome MCP warmup retry failed");
384
+ return false;
385
+ } finally {
386
+ if (warmupSessionId) {
387
+ try {
388
+ yield this.deleteSession(warmupSessionId, 5);
389
+ } catch (e) {
390
+ log.warn("Failed to delete warmup session after retries", {
391
+ error: e,
392
+ warmupSessionId
393
+ });
394
+ }
395
+ }
396
+ }
397
+ });
398
+ }
317
399
  }
318
400
  // Annotate the CommonJS export names for ESM import in node:
319
401
  0 && (module.exports = {
@@ -8,4 +8,8 @@ export interface ProxyServerOptions {
8
8
  /** OpenCode 内部设置 */
9
9
  settings?: OpenCodeSettings;
10
10
  }
11
- export declare function startProxyServer(targetUrl: string, port: number, options?: ProxyServerOptions): http.Server;
11
+ export interface ProxyServerResult {
12
+ server: http.Server;
13
+ actualPort: number;
14
+ }
15
+ export declare function startProxyServer(targetUrl: string, port: number, options?: ProxyServerOptions): Promise<ProxyServerResult>;
@@ -140,80 +140,86 @@ function generateBridgeScript(options) {
140
140
  `;
141
141
  }
142
142
  function startProxyServer(targetUrl, port, options = {}) {
143
- const target = new URL(targetUrl);
144
- const bridgeScript = generateBridgeScript(options);
145
- const server = import_http.default.createServer((req, res) => {
146
- if (req.url === "/__opencode_bridge__.js") {
147
- const body = bridgeScript;
148
- res.writeHead(200, {
149
- "content-type": "application/javascript; charset=utf-8",
150
- "cache-control": "no-store",
151
- "content-length": Buffer.byteLength(body)
152
- });
153
- res.end(body);
154
- return;
155
- }
156
- const options2 = {
157
- hostname: target.hostname,
158
- port: target.port,
159
- path: req.url,
160
- method: req.method,
161
- headers: __spreadProps(__spreadValues({}, req.headers), {
162
- host: target.host,
163
- // Don't accept compressed responses so we can modify HTML
164
- "accept-encoding": "identity"
165
- })
166
- };
167
- const proxyReq = import_http.default.request(options2, (proxyRes) => {
168
- var _a;
169
- const rawContentType = proxyRes.headers["content-type"];
170
- const contentType = Array.isArray(rawContentType) ? (_a = rawContentType[0]) != null ? _a : "" : rawContentType != null ? rawContentType : "";
171
- if (contentType.includes("text/html")) {
172
- const chunks = [];
173
- proxyRes.on("data", (chunk) => {
174
- chunks.push(chunk);
143
+ return new Promise((resolve, reject) => {
144
+ const target = new URL(targetUrl);
145
+ const bridgeScript = generateBridgeScript(options);
146
+ const server = import_http.default.createServer((req, res) => {
147
+ if (req.url === "/__opencode_bridge__.js") {
148
+ const body = bridgeScript;
149
+ res.writeHead(200, {
150
+ "content-type": "application/javascript; charset=utf-8",
151
+ "cache-control": "no-store",
152
+ "content-length": Buffer.byteLength(body)
175
153
  });
176
- proxyRes.on("end", () => {
177
- let body = Buffer.concat(chunks).toString("utf-8");
178
- if (body.match(/<\/head>/i)) {
179
- body = body.replace(
180
- /<\/head>/i,
181
- '<script src="/__opencode_bridge__.js"></script></head>'
182
- );
183
- } else if (body.match(/<\/body>/i)) {
184
- body = body.replace(
185
- /<\/body>/i,
186
- '<script src="/__opencode_bridge__.js"></script></body>'
187
- );
188
- } else {
189
- body += '<script src="/__opencode_bridge__.js"></script>';
190
- }
191
- const headers = {};
192
- for (const [key, value] of Object.entries(proxyRes.headers)) {
193
- if (value !== void 0 && key !== "content-encoding" && key !== "transfer-encoding" && key !== "content-length") {
194
- headers[key] = value;
195
- }
196
- }
197
- headers["content-length"] = Buffer.byteLength(body);
198
- res.writeHead(proxyRes.statusCode || 200, headers);
199
- res.end(body);
200
- });
201
- } else {
202
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
203
- proxyRes.pipe(res);
154
+ res.end(body);
155
+ return;
204
156
  }
157
+ const requestOptions = {
158
+ hostname: target.hostname,
159
+ port: target.port,
160
+ path: req.url,
161
+ method: req.method,
162
+ headers: __spreadProps(__spreadValues({}, req.headers), {
163
+ host: target.host,
164
+ "accept-encoding": "identity"
165
+ })
166
+ };
167
+ const proxyReq = import_http.default.request(requestOptions, (proxyRes) => {
168
+ var _a;
169
+ const rawContentType = proxyRes.headers["content-type"];
170
+ const contentType = Array.isArray(rawContentType) ? (_a = rawContentType[0]) != null ? _a : "" : rawContentType != null ? rawContentType : "";
171
+ if (contentType.includes("text/html")) {
172
+ const chunks = [];
173
+ proxyRes.on("data", (chunk) => {
174
+ chunks.push(chunk);
175
+ });
176
+ proxyRes.on("end", () => {
177
+ let body = Buffer.concat(chunks).toString("utf-8");
178
+ if (body.match(/<\/head>/i)) {
179
+ body = body.replace(
180
+ /<\/head>/i,
181
+ '<script src="/__opencode_bridge__.js"></script></head>'
182
+ );
183
+ } else if (body.match(/<\/body>/i)) {
184
+ body = body.replace(
185
+ /<\/body>/i,
186
+ '<script src="/__opencode_bridge__.js"></script></body>'
187
+ );
188
+ } else {
189
+ body += '<script src="/__opencode_bridge__.js"></script>';
190
+ }
191
+ const headers = {};
192
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
193
+ if (value !== void 0 && key !== "content-encoding" && key !== "transfer-encoding" && key !== "content-length") {
194
+ headers[key] = value;
195
+ }
196
+ }
197
+ headers["content-length"] = Buffer.byteLength(body);
198
+ res.writeHead(proxyRes.statusCode || 200, headers);
199
+ res.end(body);
200
+ });
201
+ } else {
202
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
203
+ proxyRes.pipe(res);
204
+ }
205
+ });
206
+ proxyReq.on("error", (err) => {
207
+ log.error("Proxy error", { error: err.message, url: req.url });
208
+ res.writeHead(502);
209
+ res.end("Proxy error");
210
+ });
211
+ req.pipe(proxyReq);
205
212
  });
206
- proxyReq.on("error", (err) => {
207
- log.error("Proxy error", { error: err.message, url: req.url });
208
- res.writeHead(502);
209
- res.end("Proxy error");
213
+ server.on("error", (err) => {
214
+ reject(err);
215
+ });
216
+ server.listen(port, () => {
217
+ const address = server.address();
218
+ const actualPort = typeof address === "object" && address ? address.port : port;
219
+ log.info(`Proxy server started on port ${actualPort} -> ${targetUrl}`);
220
+ resolve({ server, actualPort });
210
221
  });
211
- req.pipe(proxyReq);
212
- });
213
- server.listen(port, () => {
214
- log.info(`Proxy server started on port ${port} -> ${targetUrl}`);
215
222
  });
216
- return server;
217
223
  }
218
224
  // Annotate the CommonJS export names for ESM import in node:
219
225
  0 && (module.exports = {
@@ -1,6 +1,6 @@
1
1
  import type { ResultPromise } from "execa";
2
2
  import type http from "http";
3
- import type { OpenCodeOptions } from "@vite-plugin-opencode-assistant/shared";
3
+ import type { OpenCodeOptions, ServiceStartupTask } from "@vite-plugin-opencode-assistant/shared";
4
4
  import type { OpenCodeAPI } from "./api.js";
5
5
  export declare class OpenCodeService {
6
6
  private config;
@@ -15,7 +15,14 @@ export declare class OpenCodeService {
15
15
  private startPromise;
16
16
  sessionUrl: string | null;
17
17
  private proxyServer;
18
+ chromeMcpWarmupFailed: boolean;
19
+ currentTask: {
20
+ task: ServiceStartupTask;
21
+ data?: Record<string, unknown>;
22
+ } | null;
18
23
  constructor(config: Required<OpenCodeOptions>, api: OpenCodeAPI, sseClients: Set<http.ServerResponse>, onPortAllocated: (port: number) => void, onProxyPortAllocated: (port: number) => void);
24
+ private sendTaskUpdate;
19
25
  start(corsOrigins?: string[], contextApiUrl?: string, viteOrigin?: string): Promise<void>;
26
+ retryWarmupChromeMcp(viteOrigin?: string): Promise<boolean>;
20
27
  stop(): Promise<void>;
21
28
  }
@@ -1,8 +1,21 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
4
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
5
7
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __spreadValues = (a, b) => {
9
+ for (var prop in b || (b = {}))
10
+ if (__hasOwnProp.call(b, prop))
11
+ __defNormalProp(a, prop, b[prop]);
12
+ if (__getOwnPropSymbols)
13
+ for (var prop of __getOwnPropSymbols(b)) {
14
+ if (__propIsEnum.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ }
17
+ return a;
18
+ };
6
19
  var __export = (target, all) => {
7
20
  for (var name in all)
8
21
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -61,10 +74,24 @@ class OpenCodeService {
61
74
  __publicField(this, "startPromise", null);
62
75
  __publicField(this, "sessionUrl", null);
63
76
  __publicField(this, "proxyServer", null);
77
+ __publicField(this, "chromeMcpWarmupFailed", false);
78
+ __publicField(this, "currentTask", null);
64
79
  var _a;
65
80
  this.actualWebPort = config.webPort;
66
81
  this.actualProxyPort = (_a = config.proxyPort) != null ? _a : import_shared.DEFAULT_PROXY_PORT;
67
82
  }
83
+ sendTaskUpdate(task, data) {
84
+ this.currentTask = __spreadValues({ task }, data);
85
+ this.sseClients.forEach((client) => {
86
+ try {
87
+ client.write(`data: ${JSON.stringify(__spreadValues({ type: "TASK_UPDATE", task }, data))}
88
+
89
+ `);
90
+ } catch (e) {
91
+ log.debug("Failed to send TASK_UPDATE event", { error: e });
92
+ }
93
+ });
94
+ }
68
95
  start(corsOrigins, contextApiUrl, viteOrigin) {
69
96
  return __async(this, null, function* () {
70
97
  if (this.isStarted && this.webProcess) {
@@ -76,7 +103,7 @@ class OpenCodeService {
76
103
  return this.startPromise;
77
104
  }
78
105
  this.startPromise = (() => __async(this, null, function* () {
79
- var _a, _b, _c;
106
+ var _a, _b, _c, _d, _e;
80
107
  const timer = log.timer("startServices", {
81
108
  corsOrigins,
82
109
  contextApiUrl,
@@ -87,6 +114,7 @@ class OpenCodeService {
87
114
  if (orphanCount > 0) {
88
115
  log.debug(`Killed ${orphanCount} orphan OpenCode process(es)`);
89
116
  }
117
+ this.sendTaskUpdate("checking_opencode");
90
118
  if (!(yield (0, import_system.checkOpenCodeInstalled)())) {
91
119
  log.error(`OpenCode is not installed!
92
120
 
@@ -106,10 +134,13 @@ Please install OpenCode first:
106
134
  mise use -g opencode # Any OS
107
135
  nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
108
136
  `);
137
+ this.sendTaskUpdate("opencode_not_installed");
138
+ this.startPromise = null;
109
139
  timer.end("\u274C OpenCode not installed");
110
140
  return;
111
141
  }
112
142
  timer.checkpoint("OpenCode installation verified");
143
+ this.sendTaskUpdate("allocating_port");
113
144
  this.actualWebPort = yield (0, import_system.findAvailablePort)(this.config.webPort, this.config.hostname);
114
145
  this.onPortAllocated(this.actualWebPort);
115
146
  if (this.actualWebPort !== this.config.webPort) {
@@ -118,8 +149,10 @@ Please install OpenCode first:
118
149
  log.debug(`Using port ${this.actualWebPort}`);
119
150
  }
120
151
  timer.checkpoint("Port allocated");
152
+ this.sendTaskUpdate("preparing_runtime");
121
153
  const configDir = (0, import_opencode.prepareOpenCodeRuntime)(process.cwd());
122
154
  timer.checkpoint("Plugin setup complete");
155
+ this.sendTaskUpdate("starting_web");
123
156
  log.debug("Starting OpenCode Web process...", {
124
157
  port: this.actualWebPort,
125
158
  hostname: this.config.hostname,
@@ -137,53 +170,119 @@ Please install OpenCode first:
137
170
  timer.checkpoint("Web process started");
138
171
  const webUrl = `http://${this.config.hostname}:${this.actualWebPort}`;
139
172
  log.info(`Waiting for OpenCode Web to become ready at ${webUrl}...`);
140
- yield (0, import_system.waitForServer)(webUrl, import_shared.SERVER_START_TIMEOUT);
141
- log.info(`OpenCode Web started at ${webUrl}`);
142
- this.actualProxyPort = yield (0, import_system.findAvailablePort)(
143
- (_a = this.config.proxyPort) != null ? _a : import_shared.DEFAULT_PROXY_PORT,
144
- this.config.hostname
145
- );
173
+ this.sendTaskUpdate("waiting_web_ready");
174
+ try {
175
+ yield (0, import_system.waitForServer)(webUrl, import_shared.SERVER_START_TIMEOUT, this.webProcess);
176
+ if (((_a = this.webProcess) == null ? void 0 : _a.exitCode) !== null && ((_b = this.webProcess) == null ? void 0 : _b.exitCode) !== void 0) {
177
+ throw new Error(`OpenCode process exited with code ${this.webProcess.exitCode}`);
178
+ }
179
+ log.info(`OpenCode Web started at ${webUrl}`);
180
+ } catch (e) {
181
+ log.error("OpenCode Web failed to start", { error: e });
182
+ this.sendTaskUpdate("web_start_timeout");
183
+ this.startPromise = null;
184
+ timer.end("\u274C Web start timeout");
185
+ return;
186
+ }
187
+ this.sendTaskUpdate("starting_proxy");
188
+ let proxyStartPort = (_c = this.config.proxyPort) != null ? _c : import_shared.DEFAULT_PROXY_PORT;
189
+ if (proxyStartPort === this.actualWebPort) {
190
+ proxyStartPort = this.actualWebPort + 1;
191
+ log.debug(`Proxy start port conflicts with web port, using ${proxyStartPort} instead`);
192
+ }
193
+ this.actualProxyPort = yield (0, import_system.findAvailablePort)(proxyStartPort, this.config.hostname);
146
194
  this.onProxyPortAllocated(this.actualProxyPort);
147
- if (this.actualProxyPort !== ((_b = this.config.proxyPort) != null ? _b : import_shared.DEFAULT_PROXY_PORT)) {
195
+ if (this.actualProxyPort !== ((_d = this.config.proxyPort) != null ? _d : import_shared.DEFAULT_PROXY_PORT)) {
148
196
  log.info(
149
- `Proxy port ${(_c = this.config.proxyPort) != null ? _c : import_shared.DEFAULT_PROXY_PORT} is in use, using ${this.actualProxyPort} instead`
197
+ `Proxy port ${(_e = this.config.proxyPort) != null ? _e : import_shared.DEFAULT_PROXY_PORT} is in use, using ${this.actualProxyPort} instead`
150
198
  );
151
199
  } else {
152
200
  log.debug(`Using proxy port ${this.actualProxyPort}`);
153
201
  }
154
- this.proxyServer = (0, import_proxy_server.startProxyServer)(webUrl, this.actualProxyPort, {
155
- theme: this.config.theme,
156
- language: this.config.language,
157
- settings: this.config.settings
158
- });
202
+ try {
203
+ const result = yield (0, import_proxy_server.startProxyServer)(webUrl, this.actualProxyPort, {
204
+ theme: this.config.theme,
205
+ language: this.config.language,
206
+ settings: this.config.settings
207
+ });
208
+ this.proxyServer = result.server;
209
+ if (result.actualPort !== this.actualProxyPort) {
210
+ log.info(
211
+ `Proxy port ${this.actualProxyPort} was taken, using ${result.actualPort} instead`
212
+ );
213
+ this.actualProxyPort = result.actualPort;
214
+ this.onProxyPortAllocated(this.actualProxyPort);
215
+ }
216
+ } catch (err) {
217
+ const nodeErr = err;
218
+ if (nodeErr.code === "EADDRINUSE") {
219
+ log.warn(`Proxy port ${this.actualProxyPort} became unavailable, trying next port...`);
220
+ const nextPort = yield (0, import_system.findAvailablePort)(this.actualProxyPort + 1, this.config.hostname);
221
+ const result = yield (0, import_proxy_server.startProxyServer)(webUrl, nextPort, {
222
+ theme: this.config.theme,
223
+ language: this.config.language,
224
+ settings: this.config.settings
225
+ });
226
+ this.proxyServer = result.server;
227
+ this.actualProxyPort = result.actualPort;
228
+ this.onProxyPortAllocated(this.actualProxyPort);
229
+ log.info(`Proxy server started on fallback port ${this.actualProxyPort}`);
230
+ } else {
231
+ throw err;
232
+ }
233
+ }
159
234
  timer.checkpoint("Proxy server started");
160
- yield this.api.warmupChromeMcp(viteOrigin);
161
- timer.checkpoint("Chrome MCP warmup complete");
235
+ this.sendTaskUpdate("warming_up_chrome");
236
+ let warmupFailed = false;
237
+ try {
238
+ yield this.api.warmupChromeMcp(viteOrigin);
239
+ timer.checkpoint("Chrome MCP warmup complete");
240
+ } catch (e) {
241
+ log.warn("Chrome MCP warmup failed", { error: e });
242
+ this.chromeMcpWarmupFailed = true;
243
+ warmupFailed = true;
244
+ }
245
+ this.sendTaskUpdate("creating_session");
246
+ let sessionFailed = false;
162
247
  try {
163
248
  this.sessionUrl = yield this.api.getOrCreateSession();
164
249
  timer.checkpoint("Session created");
165
250
  log.debug(`Session URL: ${this.sessionUrl}`);
166
- this.sseClients.forEach((client) => {
167
- try {
168
- client.write(
169
- `data: ${JSON.stringify({ type: "SESSION_READY", sessionUrl: this.sessionUrl })}
170
-
171
- `
172
- );
173
- } catch (e) {
174
- log.debug("Failed to send SESSION_READY event", { error: e });
175
- }
176
- });
177
251
  } catch (e) {
178
252
  log.warn("Failed to get/create session", { error: e });
253
+ sessionFailed = true;
254
+ }
255
+ if (sessionFailed) {
256
+ this.sendTaskUpdate("session_creation_failed");
257
+ this.isStarted = false;
258
+ this.startPromise = null;
259
+ } else if (warmupFailed) {
260
+ this.sendTaskUpdate("chrome_mcp_failed", { sessionUrl: this.sessionUrl });
261
+ this.isStarted = true;
262
+ } else {
263
+ this.sendTaskUpdate("ready", { sessionUrl: this.sessionUrl });
264
+ }
265
+ if (!sessionFailed) {
266
+ this.isStarted = true;
267
+ } else {
268
+ this.sessionUrl = null;
179
269
  }
180
- this.isStarted = true;
181
270
  log.debug(`OpenCode services started successfully: ${this.sessionUrl || webUrl}`);
182
271
  timer.end("\u2713 Services started successfully");
183
272
  }))();
184
273
  return this.startPromise;
185
274
  });
186
275
  }
276
+ retryWarmupChromeMcp(viteOrigin) {
277
+ return __async(this, null, function* () {
278
+ const success = yield this.api.retryWarmupChromeMcp(viteOrigin);
279
+ if (success) {
280
+ this.chromeMcpWarmupFailed = false;
281
+ this.sendTaskUpdate("ready", { sessionUrl: this.sessionUrl });
282
+ }
283
+ return success;
284
+ });
285
+ }
187
286
  stop() {
188
287
  return __async(this, null, function* () {
189
288
  const timer = log.timer("stopServices");
@@ -26,6 +26,7 @@ var import_context = require("./context.js");
26
26
  var import_start = require("./start.js");
27
27
  var import_sse = require("./sse.js");
28
28
  var import_sessions = require("./sessions.js");
29
+ var import_warmup = require("./warmup.js");
29
30
  __reExport(endpoints_exports, require("./types.js"), module.exports);
30
31
  function setupMiddlewares(server, ctx) {
31
32
  (0, import_widget.setupWidgetEndpoints)(server, ctx);
@@ -33,6 +34,7 @@ function setupMiddlewares(server, ctx) {
33
34
  (0, import_start.setupStartEndpoint)(server, ctx);
34
35
  (0, import_sse.setupSseEndpoint)(server, ctx);
35
36
  (0, import_sessions.setupSessionsEndpoint)(server, ctx);
37
+ (0, import_warmup.setupWarmupEndpoint)(server, ctx);
36
38
  }
37
39
  // Annotate the CommonJS export names for ESM import in node:
38
40
  0 && (module.exports = {
@@ -57,13 +57,22 @@ function setupSseEndpoint(server, ctx) {
57
57
  res.write(`data: ${JSON.stringify({ type: "CONNECTED" })}
58
58
 
59
59
  `);
60
+ const statusPayload = { type: "STATUS_SYNC" };
61
+ if (ctx.isServiceStarted !== void 0) {
62
+ statusPayload.isStarted = ctx.isServiceStarted;
63
+ }
64
+ if (ctx.currentTask) {
65
+ statusPayload.task = ctx.currentTask.task;
66
+ if (ctx.currentTask.data) {
67
+ Object.assign(statusPayload, ctx.currentTask.data);
68
+ }
69
+ }
60
70
  if (ctx.sessionUrl) {
61
- res.write(
62
- `data: ${JSON.stringify({ type: "SESSION_READY", sessionUrl: ctx.sessionUrl })}
63
-
64
- `
65
- );
71
+ statusPayload.sessionUrl = ctx.sessionUrl;
66
72
  }
73
+ res.write(`data: ${JSON.stringify(statusPayload)}
74
+
75
+ `);
67
76
  req.on("close", () => {
68
77
  ctx.sseClients.delete(res);
69
78
  log.debug("SSE client disconnected", {
@@ -1,4 +1,4 @@
1
- import type { PageContext, SessionInfo } from "@vite-plugin-opencode-assistant/shared";
1
+ import type { PageContext, SessionInfo, ServiceStartupTask } from "@vite-plugin-opencode-assistant/shared";
2
2
  import type http from "http";
3
3
  export interface EndpointContext {
4
4
  get sessionUrl(): string | null;
@@ -6,9 +6,15 @@ export interface EndpointContext {
6
6
  get sseClients(): Set<http.ServerResponse>;
7
7
  get pageContext(): PageContext;
8
8
  set pageContext(ctx: PageContext);
9
+ get isServiceStarted(): boolean;
10
+ get currentTask(): {
11
+ task: ServiceStartupTask;
12
+ data?: Record<string, unknown>;
13
+ } | null;
9
14
  getSessions: () => Promise<SessionInfo[]>;
10
15
  createSession: () => Promise<SessionInfo>;
11
16
  deleteSession: (id: string) => Promise<void>;
12
17
  resolveWidgetPath: () => string;
13
18
  resolveWidgetStylePath: () => string;
19
+ retryWarmupChromeMcp: () => Promise<boolean>;
14
20
  }
@@ -0,0 +1,3 @@
1
+ import type { ViteDevServer } from "vite";
2
+ import type { EndpointContext } from "./types.js";
3
+ export declare function setupWarmupEndpoint(server: ViteDevServer, ctx: EndpointContext): void;