vite-plugin-opencode-assistant 1.0.13 → 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.
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
@@ -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) {
@@ -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,8 +159,18 @@ 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}`);
162
+ this.sendTaskUpdate("waiting_web_ready");
163
+ try {
164
+ yield waitForServer(webUrl, SERVER_START_TIMEOUT);
165
+ log.info(`OpenCode Web started at ${webUrl}`);
166
+ } catch (e) {
167
+ log.error("OpenCode Web failed to start", { error: e });
168
+ this.sendTaskUpdate("web_start_timeout");
169
+ this.startPromise = null;
170
+ timer.end("\u274C Web start timeout");
171
+ return;
172
+ }
173
+ this.sendTaskUpdate("starting_proxy");
130
174
  this.actualProxyPort = yield findAvailablePort(
131
175
  (_a = this.config.proxyPort) != null ? _a : DEFAULT_PROXY_PORT,
132
176
  this.config.hostname
@@ -145,33 +189,57 @@ Please install OpenCode first:
145
189
  settings: this.config.settings
146
190
  });
147
191
  timer.checkpoint("Proxy server started");
148
- yield this.api.warmupChromeMcp(viteOrigin);
149
- timer.checkpoint("Chrome MCP warmup complete");
192
+ this.sendTaskUpdate("warming_up_chrome");
193
+ let warmupFailed = false;
194
+ try {
195
+ yield this.api.warmupChromeMcp(viteOrigin);
196
+ timer.checkpoint("Chrome MCP warmup complete");
197
+ } catch (e) {
198
+ log.warn("Chrome MCP warmup failed", { error: e });
199
+ this.chromeMcpWarmupFailed = true;
200
+ warmupFailed = true;
201
+ }
202
+ this.sendTaskUpdate("creating_session");
203
+ let sessionFailed = false;
150
204
  try {
151
205
  this.sessionUrl = yield this.api.getOrCreateSession();
152
206
  timer.checkpoint("Session created");
153
207
  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
208
  } catch (e) {
166
209
  log.warn("Failed to get/create session", { error: e });
210
+ sessionFailed = true;
211
+ }
212
+ if (sessionFailed) {
213
+ this.sendTaskUpdate("session_creation_failed");
214
+ this.isStarted = false;
215
+ this.startPromise = null;
216
+ } else if (warmupFailed) {
217
+ this.sendTaskUpdate("chrome_mcp_failed", { sessionUrl: this.sessionUrl });
218
+ this.isStarted = true;
219
+ } else {
220
+ this.sendTaskUpdate("ready", { sessionUrl: this.sessionUrl });
221
+ }
222
+ if (!sessionFailed) {
223
+ this.isStarted = true;
224
+ } else {
225
+ this.sessionUrl = null;
167
226
  }
168
- this.isStarted = true;
169
227
  log.debug(`OpenCode services started successfully: ${this.sessionUrl || webUrl}`);
170
228
  timer.end("\u2713 Services started successfully");
171
229
  }))();
172
230
  return this.startPromise;
173
231
  });
174
232
  }
233
+ retryWarmupChromeMcp(viteOrigin) {
234
+ return __async(this, null, function* () {
235
+ const success = yield this.api.retryWarmupChromeMcp(viteOrigin);
236
+ if (success) {
237
+ this.chromeMcpWarmupFailed = false;
238
+ this.sendTaskUpdate("ready", { sessionUrl: this.sessionUrl });
239
+ }
240
+ return success;
241
+ });
242
+ }
175
243
  stop() {
176
244
  return __async(this, null, function* () {
177
245
  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;
@@ -0,0 +1,45 @@
1
+ var __async = (__this, __arguments, generator) => {
2
+ return new Promise((resolve, reject) => {
3
+ var fulfilled = (value) => {
4
+ try {
5
+ step(generator.next(value));
6
+ } catch (e) {
7
+ reject(e);
8
+ }
9
+ };
10
+ var rejected = (value) => {
11
+ try {
12
+ step(generator.throw(value));
13
+ } catch (e) {
14
+ reject(e);
15
+ }
16
+ };
17
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
18
+ step((generator = generator.apply(__this, __arguments)).next());
19
+ });
20
+ };
21
+ import { createLogger } from "@vite-plugin-opencode-assistant/shared";
22
+ const log = createLogger("Endpoints:Warmup");
23
+ function setupWarmupEndpoint(server, ctx) {
24
+ server.middlewares.use("/__opencode_warmup__", (req, res) => __async(null, null, function* () {
25
+ if (req.method !== "POST") {
26
+ res.writeHead(405);
27
+ res.end("Method not allowed");
28
+ return;
29
+ }
30
+ try {
31
+ const success = yield ctx.retryWarmupChromeMcp();
32
+ res.setHeader("Content-Type", "application/json");
33
+ res.writeHead(200);
34
+ res.end(JSON.stringify({ success }));
35
+ } catch (e) {
36
+ log.error("Failed to retry warmup", { error: e });
37
+ res.setHeader("Content-Type", "application/json");
38
+ res.writeHead(500);
39
+ res.end(JSON.stringify({ success: false, error: String(e) }));
40
+ }
41
+ }));
42
+ }
43
+ export {
44
+ setupWarmupEndpoint
45
+ };
package/es/index.js CHANGED
@@ -90,6 +90,8 @@ function createOpenCodePlugin(options = {}) {
90
90
  return __async(this, null, function* () {
91
91
  var _a2, _b2;
92
92
  const timer = log.timer("configureServer");
93
+ let viteOrigin = "";
94
+ const getViteOrigin = () => viteOrigin;
93
95
  setupMiddlewares(server, {
94
96
  get sessionUrl() {
95
97
  return service.sessionUrl;
@@ -106,11 +108,18 @@ function createOpenCodePlugin(options = {}) {
106
108
  set pageContext(ctx) {
107
109
  pageContext = ctx;
108
110
  },
111
+ get isServiceStarted() {
112
+ return service.isStarted;
113
+ },
114
+ get currentTask() {
115
+ return service.currentTask;
116
+ },
109
117
  getSessions: () => api.getSessions(),
110
118
  createSession: () => api.createSession(),
111
119
  deleteSession: (id) => api.deleteSession(id),
112
120
  resolveWidgetPath,
113
- resolveWidgetStylePath
121
+ resolveWidgetStylePath,
122
+ retryWarmupChromeMcp: () => service.retryWarmupChromeMcp(getViteOrigin())
114
123
  });
115
124
  (_a2 = server.httpServer) == null ? void 0 : _a2.on("listening", () => __async(null, null, function* () {
116
125
  var _a3;
@@ -131,7 +140,7 @@ function createOpenCodePlugin(options = {}) {
131
140
  vitePort = server.config.server.port || 5173;
132
141
  viteHost = typeof host === "string" && host !== "0.0.0.0" && host !== "::" && host !== "::1" ? host : "localhost";
133
142
  }
134
- const viteOrigin = `http://${viteHost}:${vitePort}`;
143
+ viteOrigin = `http://${viteHost}:${vitePort}`;
135
144
  const contextApiUrl = `http://${viteHost}:${vitePort}${CONTEXT_API_PATH}`;
136
145
  log.debug("Vite server ready", {
137
146
  vitePort,
@@ -170,7 +179,7 @@ function createOpenCodePlugin(options = {}) {
170
179
  open: config.open,
171
180
  autoReload: config.autoReload,
172
181
  cwd: process.cwd(),
173
- sessionUrl: service.sessionUrl || void 0,
182
+ // 不再注入 sessionUrl,客户端完全依赖 SSE 状态同步
174
183
  hotkey: config.hotkey
175
184
  });
176
185
  timer.end();