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/es/core/api.js CHANGED
@@ -35,13 +35,36 @@ function sleep(ms) {
35
35
  function base64Encode(str) {
36
36
  return Buffer.from(str).toString("base64");
37
37
  }
38
+ function extractTextFromResponse(data) {
39
+ if (!data || typeof data !== "object") return null;
40
+ const obj = data;
41
+ if (obj.parts && Array.isArray(obj.parts)) {
42
+ const textParts = obj.parts.filter(
43
+ (p) => p && typeof p === "object" && p.type === "text"
44
+ ).map((p) => p.text).filter(Boolean);
45
+ if (textParts.length > 0) return textParts.join("");
46
+ }
47
+ if (obj.text && typeof obj.text === "string") {
48
+ return obj.text;
49
+ }
50
+ if (obj.content && typeof obj.content === "string") {
51
+ return obj.content;
52
+ }
53
+ if (obj.message && typeof obj.message === "string") {
54
+ return obj.message;
55
+ }
56
+ if (typeof data === "string") {
57
+ return data;
58
+ }
59
+ return null;
60
+ }
38
61
  class OpenCodeAPI {
39
62
  constructor(hostname, getPort, warmupChromeMcpConfig = false) {
40
63
  __publicField(this, "hostname", hostname);
41
64
  __publicField(this, "getPort", getPort);
42
65
  __publicField(this, "warmupChromeMcpConfig", warmupChromeMcpConfig);
43
66
  }
44
- createHttpRequest(options, body) {
67
+ createHttpRequest(options, body, timeout) {
45
68
  const timer = new PerformanceTimer("HTTP Request", {
46
69
  operation: `${options.method || "GET"} ${options.path}`
47
70
  });
@@ -64,6 +87,13 @@ class OpenCodeAPI {
64
87
  timer.end("\u274C Request failed");
65
88
  reject(e);
66
89
  });
90
+ if (timeout) {
91
+ req.setTimeout(timeout, () => {
92
+ timer.end("\u274C Request timeout");
93
+ req.destroy();
94
+ reject(new Error(`Request timeout after ${timeout}ms`));
95
+ });
96
+ }
67
97
  if (body) req.write(body);
68
98
  req.end();
69
99
  });
@@ -217,24 +247,16 @@ class OpenCodeAPI {
217
247
  try {
218
248
  const warmupSession = yield this.createSession(DEFAULT_RETRIES, "__chrome_mcp_warmup__");
219
249
  warmupSessionId = warmupSession.id;
220
- let chromeToolIds;
221
- try {
222
- const toolIds = yield this.getToolIds();
223
- chromeToolIds = toolIds.filter((toolId) => /chrome[-_]?devtools/i.test(toolId));
224
- log.debug("Resolved Chrome MCP tool ids", {
225
- chromeToolIds
226
- });
227
- } catch (e) {
228
- log.debug("Failed to resolve Chrome MCP tool ids", { error: e });
229
- }
230
250
  const prompt = [
231
251
  "Call the browser tool list_pages immediately to establish the Chrome DevTools MCP connection.",
232
252
  viteOrigin ? `If there are no pages, call new_page with ${viteOrigin}.` : "If there are no pages, call new_page with about:blank.",
233
253
  "Do not read or modify project files.",
234
254
  "Do not use any non-browser tools.",
235
- "After the tool call is complete, reply with exactly: ready"
255
+ "After the tool call is complete, reply with exactly: ready",
256
+ "If the tool call fails, reply with exactly: fail"
236
257
  ].join(" ");
237
- yield this.createHttpRequest(
258
+ const WARMUP_TIMEOUT = 3e4;
259
+ const data = yield this.createHttpRequest(
238
260
  {
239
261
  hostname: this.hostname,
240
262
  port: this.getPort(),
@@ -244,14 +266,19 @@ class OpenCodeAPI {
244
266
  },
245
267
  JSON.stringify({
246
268
  system: "You are warming up Chrome DevTools MCP during startup. You must use the available browser tools immediately before replying.",
247
- tools: (chromeToolIds == null ? void 0 : chromeToolIds.length) ? chromeToolIds : void 0,
248
269
  parts: [{ type: "text", text: prompt }]
249
- })
270
+ }),
271
+ WARMUP_TIMEOUT
250
272
  );
273
+ const responseText = extractTextFromResponse(data);
274
+ if (!(responseText == null ? void 0 : responseText.toLowerCase().includes("ready"))) {
275
+ throw new Error(`Chrome MCP warmup failed: ${responseText || "No response"}`);
276
+ }
251
277
  timer.end("Chrome MCP warmed up");
252
278
  } catch (e) {
253
279
  log.warn("Failed to warm up Chrome MCP", { error: e });
254
280
  timer.end("Chrome MCP warmup skipped");
281
+ throw e;
255
282
  } finally {
256
283
  if (warmupSessionId) {
257
284
  try {
@@ -288,6 +315,61 @@ class OpenCodeAPI {
288
315
  return url;
289
316
  });
290
317
  }
318
+ retryWarmupChromeMcp(viteOrigin) {
319
+ return __async(this, null, function* () {
320
+ const timer = log.timer("retryWarmupChromeMcp", { viteOrigin });
321
+ let warmupSessionId = null;
322
+ try {
323
+ const warmupSession = yield this.createSession(DEFAULT_RETRIES, "__chrome_mcp_warmup__");
324
+ warmupSessionId = warmupSession.id;
325
+ const prompt = [
326
+ "Call the browser tool list_pages immediately to establish the Chrome DevTools MCP connection.",
327
+ viteOrigin ? `If there are no pages, call new_page with ${viteOrigin}.` : "If there are no pages, call new_page with about:blank.",
328
+ "Do not read or modify project files.",
329
+ "Do not use any non-browser tools.",
330
+ "After the tool call is complete, reply with exactly: ready",
331
+ "If the tool call fails, reply with exactly: fail"
332
+ ].join(" ");
333
+ const WARMUP_TIMEOUT = 6e4;
334
+ const data = yield this.createHttpRequest(
335
+ {
336
+ hostname: this.hostname,
337
+ port: this.getPort(),
338
+ path: `/session/${warmupSessionId}/message`,
339
+ method: "POST",
340
+ headers: { "Content-Type": "application/json" }
341
+ },
342
+ JSON.stringify({
343
+ system: "You are warming up Chrome DevTools MCP during startup. You must use the available browser tools immediately before replying.",
344
+ parts: [{ type: "text", text: prompt }]
345
+ }),
346
+ WARMUP_TIMEOUT
347
+ );
348
+ log.debug("Chrome MCP warmup response:", { data });
349
+ const responseText = extractTextFromResponse(data);
350
+ if (!(responseText == null ? void 0 : responseText.toLowerCase().includes("ready"))) {
351
+ throw new Error(`Chrome MCP warmup failed: ${responseText || "No response"}`);
352
+ }
353
+ timer.end("Chrome MCP warmed up successfully");
354
+ return true;
355
+ } catch (e) {
356
+ log.warn("Failed to retry warm up Chrome MCP", { error: e });
357
+ timer.end("Chrome MCP warmup retry failed");
358
+ return false;
359
+ } finally {
360
+ if (warmupSessionId) {
361
+ try {
362
+ yield this.deleteSession(warmupSessionId, 5);
363
+ } catch (e) {
364
+ log.warn("Failed to delete warmup session after retries", {
365
+ error: e,
366
+ warmupSessionId
367
+ });
368
+ }
369
+ }
370
+ }
371
+ });
372
+ }
291
373
  }
292
374
  export {
293
375
  OpenCodeAPI
@@ -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>;
@@ -114,80 +114,86 @@ function generateBridgeScript(options) {
114
114
  `;
115
115
  }
116
116
  function startProxyServer(targetUrl, port, options = {}) {
117
- const target = new URL(targetUrl);
118
- const bridgeScript = generateBridgeScript(options);
119
- const server = http.createServer((req, res) => {
120
- if (req.url === "/__opencode_bridge__.js") {
121
- const body = bridgeScript;
122
- res.writeHead(200, {
123
- "content-type": "application/javascript; charset=utf-8",
124
- "cache-control": "no-store",
125
- "content-length": Buffer.byteLength(body)
126
- });
127
- res.end(body);
128
- return;
129
- }
130
- const options2 = {
131
- hostname: target.hostname,
132
- port: target.port,
133
- path: req.url,
134
- method: req.method,
135
- headers: __spreadProps(__spreadValues({}, req.headers), {
136
- host: target.host,
137
- // Don't accept compressed responses so we can modify HTML
138
- "accept-encoding": "identity"
139
- })
140
- };
141
- const proxyReq = http.request(options2, (proxyRes) => {
142
- var _a;
143
- const rawContentType = proxyRes.headers["content-type"];
144
- const contentType = Array.isArray(rawContentType) ? (_a = rawContentType[0]) != null ? _a : "" : rawContentType != null ? rawContentType : "";
145
- if (contentType.includes("text/html")) {
146
- const chunks = [];
147
- proxyRes.on("data", (chunk) => {
148
- chunks.push(chunk);
117
+ return new Promise((resolve, reject) => {
118
+ const target = new URL(targetUrl);
119
+ const bridgeScript = generateBridgeScript(options);
120
+ const server = http.createServer((req, res) => {
121
+ if (req.url === "/__opencode_bridge__.js") {
122
+ const body = bridgeScript;
123
+ res.writeHead(200, {
124
+ "content-type": "application/javascript; charset=utf-8",
125
+ "cache-control": "no-store",
126
+ "content-length": Buffer.byteLength(body)
149
127
  });
150
- proxyRes.on("end", () => {
151
- let body = Buffer.concat(chunks).toString("utf-8");
152
- if (body.match(/<\/head>/i)) {
153
- body = body.replace(
154
- /<\/head>/i,
155
- '<script src="/__opencode_bridge__.js"></script></head>'
156
- );
157
- } else if (body.match(/<\/body>/i)) {
158
- body = body.replace(
159
- /<\/body>/i,
160
- '<script src="/__opencode_bridge__.js"></script></body>'
161
- );
162
- } else {
163
- body += '<script src="/__opencode_bridge__.js"></script>';
164
- }
165
- const headers = {};
166
- for (const [key, value] of Object.entries(proxyRes.headers)) {
167
- if (value !== void 0 && key !== "content-encoding" && key !== "transfer-encoding" && key !== "content-length") {
168
- headers[key] = value;
169
- }
170
- }
171
- headers["content-length"] = Buffer.byteLength(body);
172
- res.writeHead(proxyRes.statusCode || 200, headers);
173
- res.end(body);
174
- });
175
- } else {
176
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
177
- proxyRes.pipe(res);
128
+ res.end(body);
129
+ return;
178
130
  }
131
+ const requestOptions = {
132
+ hostname: target.hostname,
133
+ port: target.port,
134
+ path: req.url,
135
+ method: req.method,
136
+ headers: __spreadProps(__spreadValues({}, req.headers), {
137
+ host: target.host,
138
+ "accept-encoding": "identity"
139
+ })
140
+ };
141
+ const proxyReq = http.request(requestOptions, (proxyRes) => {
142
+ var _a;
143
+ const rawContentType = proxyRes.headers["content-type"];
144
+ const contentType = Array.isArray(rawContentType) ? (_a = rawContentType[0]) != null ? _a : "" : rawContentType != null ? rawContentType : "";
145
+ if (contentType.includes("text/html")) {
146
+ const chunks = [];
147
+ proxyRes.on("data", (chunk) => {
148
+ chunks.push(chunk);
149
+ });
150
+ proxyRes.on("end", () => {
151
+ let body = Buffer.concat(chunks).toString("utf-8");
152
+ if (body.match(/<\/head>/i)) {
153
+ body = body.replace(
154
+ /<\/head>/i,
155
+ '<script src="/__opencode_bridge__.js"></script></head>'
156
+ );
157
+ } else if (body.match(/<\/body>/i)) {
158
+ body = body.replace(
159
+ /<\/body>/i,
160
+ '<script src="/__opencode_bridge__.js"></script></body>'
161
+ );
162
+ } else {
163
+ body += '<script src="/__opencode_bridge__.js"></script>';
164
+ }
165
+ const headers = {};
166
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
167
+ if (value !== void 0 && key !== "content-encoding" && key !== "transfer-encoding" && key !== "content-length") {
168
+ headers[key] = value;
169
+ }
170
+ }
171
+ headers["content-length"] = Buffer.byteLength(body);
172
+ res.writeHead(proxyRes.statusCode || 200, headers);
173
+ res.end(body);
174
+ });
175
+ } else {
176
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
177
+ proxyRes.pipe(res);
178
+ }
179
+ });
180
+ proxyReq.on("error", (err) => {
181
+ log.error("Proxy error", { error: err.message, url: req.url });
182
+ res.writeHead(502);
183
+ res.end("Proxy error");
184
+ });
185
+ req.pipe(proxyReq);
179
186
  });
180
- proxyReq.on("error", (err) => {
181
- log.error("Proxy error", { error: err.message, url: req.url });
182
- res.writeHead(502);
183
- res.end("Proxy error");
187
+ server.on("error", (err) => {
188
+ reject(err);
189
+ });
190
+ server.listen(port, () => {
191
+ const address = server.address();
192
+ const actualPort = typeof address === "object" && address ? address.port : port;
193
+ log.info(`Proxy server started on port ${actualPort} -> ${targetUrl}`);
194
+ resolve({ server, actualPort });
184
195
  });
185
- req.pipe(proxyReq);
186
- });
187
- server.listen(port, () => {
188
- log.info(`Proxy server started on port ${port} -> ${targetUrl}`);
189
196
  });
190
- return server;
191
197
  }
192
198
  export {
193
199
  startProxyServer
@@ -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,5 +1,19 @@
1
1
  var __defProp = Object.defineProperty;
2
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
3
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
2
5
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
+ var __spreadValues = (a, b) => {
7
+ for (var prop in b || (b = {}))
8
+ if (__hasOwnProp.call(b, prop))
9
+ __defNormalProp(a, prop, b[prop]);
10
+ if (__getOwnPropSymbols)
11
+ for (var prop of __getOwnPropSymbols(b)) {
12
+ if (__propIsEnum.call(b, prop))
13
+ __defNormalProp(a, prop, b[prop]);
14
+ }
15
+ return a;
16
+ };
3
17
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
18
  var __async = (__this, __arguments, generator) => {
5
19
  return new Promise((resolve, reject) => {
@@ -49,10 +63,24 @@ class OpenCodeService {
49
63
  __publicField(this, "startPromise", null);
50
64
  __publicField(this, "sessionUrl", null);
51
65
  __publicField(this, "proxyServer", null);
66
+ __publicField(this, "chromeMcpWarmupFailed", false);
67
+ __publicField(this, "currentTask", null);
52
68
  var _a;
53
69
  this.actualWebPort = config.webPort;
54
70
  this.actualProxyPort = (_a = config.proxyPort) != null ? _a : DEFAULT_PROXY_PORT;
55
71
  }
72
+ sendTaskUpdate(task, data) {
73
+ this.currentTask = __spreadValues({ task }, data);
74
+ this.sseClients.forEach((client) => {
75
+ try {
76
+ client.write(`data: ${JSON.stringify(__spreadValues({ type: "TASK_UPDATE", task }, data))}
77
+
78
+ `);
79
+ } catch (e) {
80
+ log.debug("Failed to send TASK_UPDATE event", { error: e });
81
+ }
82
+ });
83
+ }
56
84
  start(corsOrigins, contextApiUrl, viteOrigin) {
57
85
  return __async(this, null, function* () {
58
86
  if (this.isStarted && this.webProcess) {
@@ -64,7 +92,7 @@ class OpenCodeService {
64
92
  return this.startPromise;
65
93
  }
66
94
  this.startPromise = (() => __async(this, null, function* () {
67
- var _a, _b, _c;
95
+ var _a, _b, _c, _d, _e;
68
96
  const timer = log.timer("startServices", {
69
97
  corsOrigins,
70
98
  contextApiUrl,
@@ -75,6 +103,7 @@ class OpenCodeService {
75
103
  if (orphanCount > 0) {
76
104
  log.debug(`Killed ${orphanCount} orphan OpenCode process(es)`);
77
105
  }
106
+ this.sendTaskUpdate("checking_opencode");
78
107
  if (!(yield checkOpenCodeInstalled())) {
79
108
  log.error(`OpenCode is not installed!
80
109
 
@@ -94,10 +123,13 @@ Please install OpenCode first:
94
123
  mise use -g opencode # Any OS
95
124
  nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
96
125
  `);
126
+ this.sendTaskUpdate("opencode_not_installed");
127
+ this.startPromise = null;
97
128
  timer.end("\u274C OpenCode not installed");
98
129
  return;
99
130
  }
100
131
  timer.checkpoint("OpenCode installation verified");
132
+ this.sendTaskUpdate("allocating_port");
101
133
  this.actualWebPort = yield findAvailablePort(this.config.webPort, this.config.hostname);
102
134
  this.onPortAllocated(this.actualWebPort);
103
135
  if (this.actualWebPort !== this.config.webPort) {
@@ -106,8 +138,10 @@ Please install OpenCode first:
106
138
  log.debug(`Using port ${this.actualWebPort}`);
107
139
  }
108
140
  timer.checkpoint("Port allocated");
141
+ this.sendTaskUpdate("preparing_runtime");
109
142
  const configDir = prepareOpenCodeRuntime(process.cwd());
110
143
  timer.checkpoint("Plugin setup complete");
144
+ this.sendTaskUpdate("starting_web");
111
145
  log.debug("Starting OpenCode Web process...", {
112
146
  port: this.actualWebPort,
113
147
  hostname: this.config.hostname,
@@ -125,53 +159,119 @@ Please install OpenCode first:
125
159
  timer.checkpoint("Web process started");
126
160
  const webUrl = `http://${this.config.hostname}:${this.actualWebPort}`;
127
161
  log.info(`Waiting for OpenCode Web to become ready at ${webUrl}...`);
128
- yield waitForServer(webUrl, SERVER_START_TIMEOUT);
129
- log.info(`OpenCode Web started at ${webUrl}`);
130
- this.actualProxyPort = yield findAvailablePort(
131
- (_a = this.config.proxyPort) != null ? _a : DEFAULT_PROXY_PORT,
132
- this.config.hostname
133
- );
162
+ this.sendTaskUpdate("waiting_web_ready");
163
+ try {
164
+ yield waitForServer(webUrl, SERVER_START_TIMEOUT, this.webProcess);
165
+ if (((_a = this.webProcess) == null ? void 0 : _a.exitCode) !== null && ((_b = this.webProcess) == null ? void 0 : _b.exitCode) !== void 0) {
166
+ throw new Error(`OpenCode process exited with code ${this.webProcess.exitCode}`);
167
+ }
168
+ log.info(`OpenCode Web started at ${webUrl}`);
169
+ } catch (e) {
170
+ log.error("OpenCode Web failed to start", { error: e });
171
+ this.sendTaskUpdate("web_start_timeout");
172
+ this.startPromise = null;
173
+ timer.end("\u274C Web start timeout");
174
+ return;
175
+ }
176
+ this.sendTaskUpdate("starting_proxy");
177
+ let proxyStartPort = (_c = this.config.proxyPort) != null ? _c : DEFAULT_PROXY_PORT;
178
+ if (proxyStartPort === this.actualWebPort) {
179
+ proxyStartPort = this.actualWebPort + 1;
180
+ log.debug(`Proxy start port conflicts with web port, using ${proxyStartPort} instead`);
181
+ }
182
+ this.actualProxyPort = yield findAvailablePort(proxyStartPort, this.config.hostname);
134
183
  this.onProxyPortAllocated(this.actualProxyPort);
135
- if (this.actualProxyPort !== ((_b = this.config.proxyPort) != null ? _b : DEFAULT_PROXY_PORT)) {
184
+ if (this.actualProxyPort !== ((_d = this.config.proxyPort) != null ? _d : DEFAULT_PROXY_PORT)) {
136
185
  log.info(
137
- `Proxy port ${(_c = this.config.proxyPort) != null ? _c : DEFAULT_PROXY_PORT} is in use, using ${this.actualProxyPort} instead`
186
+ `Proxy port ${(_e = this.config.proxyPort) != null ? _e : DEFAULT_PROXY_PORT} is in use, using ${this.actualProxyPort} instead`
138
187
  );
139
188
  } else {
140
189
  log.debug(`Using proxy port ${this.actualProxyPort}`);
141
190
  }
142
- this.proxyServer = startProxyServer(webUrl, this.actualProxyPort, {
143
- theme: this.config.theme,
144
- language: this.config.language,
145
- settings: this.config.settings
146
- });
191
+ try {
192
+ const result = yield startProxyServer(webUrl, this.actualProxyPort, {
193
+ theme: this.config.theme,
194
+ language: this.config.language,
195
+ settings: this.config.settings
196
+ });
197
+ this.proxyServer = result.server;
198
+ if (result.actualPort !== this.actualProxyPort) {
199
+ log.info(
200
+ `Proxy port ${this.actualProxyPort} was taken, using ${result.actualPort} instead`
201
+ );
202
+ this.actualProxyPort = result.actualPort;
203
+ this.onProxyPortAllocated(this.actualProxyPort);
204
+ }
205
+ } catch (err) {
206
+ const nodeErr = err;
207
+ if (nodeErr.code === "EADDRINUSE") {
208
+ log.warn(`Proxy port ${this.actualProxyPort} became unavailable, trying next port...`);
209
+ const nextPort = yield findAvailablePort(this.actualProxyPort + 1, this.config.hostname);
210
+ const result = yield startProxyServer(webUrl, nextPort, {
211
+ theme: this.config.theme,
212
+ language: this.config.language,
213
+ settings: this.config.settings
214
+ });
215
+ this.proxyServer = result.server;
216
+ this.actualProxyPort = result.actualPort;
217
+ this.onProxyPortAllocated(this.actualProxyPort);
218
+ log.info(`Proxy server started on fallback port ${this.actualProxyPort}`);
219
+ } else {
220
+ throw err;
221
+ }
222
+ }
147
223
  timer.checkpoint("Proxy server started");
148
- yield this.api.warmupChromeMcp(viteOrigin);
149
- timer.checkpoint("Chrome MCP warmup complete");
224
+ this.sendTaskUpdate("warming_up_chrome");
225
+ let warmupFailed = false;
226
+ try {
227
+ yield this.api.warmupChromeMcp(viteOrigin);
228
+ timer.checkpoint("Chrome MCP warmup complete");
229
+ } catch (e) {
230
+ log.warn("Chrome MCP warmup failed", { error: e });
231
+ this.chromeMcpWarmupFailed = true;
232
+ warmupFailed = true;
233
+ }
234
+ this.sendTaskUpdate("creating_session");
235
+ let sessionFailed = false;
150
236
  try {
151
237
  this.sessionUrl = yield this.api.getOrCreateSession();
152
238
  timer.checkpoint("Session created");
153
239
  log.debug(`Session URL: ${this.sessionUrl}`);
154
- this.sseClients.forEach((client) => {
155
- try {
156
- client.write(
157
- `data: ${JSON.stringify({ type: "SESSION_READY", sessionUrl: this.sessionUrl })}
158
-
159
- `
160
- );
161
- } catch (e) {
162
- log.debug("Failed to send SESSION_READY event", { error: e });
163
- }
164
- });
165
240
  } catch (e) {
166
241
  log.warn("Failed to get/create session", { error: e });
242
+ sessionFailed = true;
243
+ }
244
+ if (sessionFailed) {
245
+ this.sendTaskUpdate("session_creation_failed");
246
+ this.isStarted = false;
247
+ this.startPromise = null;
248
+ } else if (warmupFailed) {
249
+ this.sendTaskUpdate("chrome_mcp_failed", { sessionUrl: this.sessionUrl });
250
+ this.isStarted = true;
251
+ } else {
252
+ this.sendTaskUpdate("ready", { sessionUrl: this.sessionUrl });
253
+ }
254
+ if (!sessionFailed) {
255
+ this.isStarted = true;
256
+ } else {
257
+ this.sessionUrl = null;
167
258
  }
168
- this.isStarted = true;
169
259
  log.debug(`OpenCode services started successfully: ${this.sessionUrl || webUrl}`);
170
260
  timer.end("\u2713 Services started successfully");
171
261
  }))();
172
262
  return this.startPromise;
173
263
  });
174
264
  }
265
+ retryWarmupChromeMcp(viteOrigin) {
266
+ return __async(this, null, function* () {
267
+ const success = yield this.api.retryWarmupChromeMcp(viteOrigin);
268
+ if (success) {
269
+ this.chromeMcpWarmupFailed = false;
270
+ this.sendTaskUpdate("ready", { sessionUrl: this.sessionUrl });
271
+ }
272
+ return success;
273
+ });
274
+ }
175
275
  stop() {
176
276
  return __async(this, null, function* () {
177
277
  const timer = log.timer("stopServices");
@@ -3,6 +3,7 @@ import { setupContextEndpoint } from "./context.js";
3
3
  import { setupStartEndpoint } from "./start.js";
4
4
  import { setupSseEndpoint } from "./sse.js";
5
5
  import { setupSessionsEndpoint } from "./sessions.js";
6
+ import { setupWarmupEndpoint } from "./warmup.js";
6
7
  export * from "./types.js";
7
8
  function setupMiddlewares(server, ctx) {
8
9
  setupWidgetEndpoints(server, ctx);
@@ -10,6 +11,7 @@ function setupMiddlewares(server, ctx) {
10
11
  setupStartEndpoint(server, ctx);
11
12
  setupSseEndpoint(server, ctx);
12
13
  setupSessionsEndpoint(server, ctx);
14
+ setupWarmupEndpoint(server, ctx);
13
15
  }
14
16
  export {
15
17
  setupMiddlewares
@@ -35,13 +35,22 @@ function setupSseEndpoint(server, ctx) {
35
35
  res.write(`data: ${JSON.stringify({ type: "CONNECTED" })}
36
36
 
37
37
  `);
38
+ const statusPayload = { type: "STATUS_SYNC" };
39
+ if (ctx.isServiceStarted !== void 0) {
40
+ statusPayload.isStarted = ctx.isServiceStarted;
41
+ }
42
+ if (ctx.currentTask) {
43
+ statusPayload.task = ctx.currentTask.task;
44
+ if (ctx.currentTask.data) {
45
+ Object.assign(statusPayload, ctx.currentTask.data);
46
+ }
47
+ }
38
48
  if (ctx.sessionUrl) {
39
- res.write(
40
- `data: ${JSON.stringify({ type: "SESSION_READY", sessionUrl: ctx.sessionUrl })}
41
-
42
- `
43
- );
49
+ statusPayload.sessionUrl = ctx.sessionUrl;
44
50
  }
51
+ res.write(`data: ${JSON.stringify(statusPayload)}
52
+
53
+ `);
45
54
  req.on("close", () => {
46
55
  ctx.sseClients.delete(res);
47
56
  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;