magpie-agent-console 1.1.0 → 1.2.0

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.
@@ -91,6 +91,31 @@ var init_log = __esm({
91
91
  }
92
92
  });
93
93
 
94
+ // daemon/src/security.ts
95
+ import { timingSafeEqual, createHash } from "node:crypto";
96
+ function checkHost(host, port) {
97
+ if (!host) return { ok: false, code: 421, message: "Host \u4E0D\u5BF9" };
98
+ const h = host.toLowerCase();
99
+ if (h === `127.0.0.1:${port}` || h === `localhost:${port}`) return { ok: true };
100
+ return { ok: false, code: 421, message: "Host \u4E0D\u5BF9" };
101
+ }
102
+ function classifyOrigin(origin, port) {
103
+ if (origin === void 0 || origin === "") return "none";
104
+ if (origin === "null") return "opaque";
105
+ const o = origin.toLowerCase();
106
+ if (o === `http://127.0.0.1:${port}` || o === `http://localhost:${port}`) return "self";
107
+ return "foreign";
108
+ }
109
+ function tokenEqual(a, b) {
110
+ const ha = createHash("sha256").update(a).digest();
111
+ const hb = createHash("sha256").update(b).digest();
112
+ return timingSafeEqual(ha, hb);
113
+ }
114
+ var init_security = __esm({
115
+ "daemon/src/security.ts"() {
116
+ }
117
+ });
118
+
94
119
  // daemon/src/util/i18n.ts
95
120
  function normalizeLang(raw) {
96
121
  const s = String(raw ?? "").trim().toLowerCase();
@@ -129,6 +154,7 @@ var init_i18n = __esm({
129
154
  "notice.acpImageByPathSwitched": "{agent} \u662F\u5728\u53E6\u4E00\u4E2A\u6A21\u578B\u4E0A\u542F\u52A8\u7684\uFF0C\u53EA\u5BF9\u90A3\u4E2A\u6A21\u578B\u63A5\u6536\u56FE\u7247\uFF0C\u8FD9\u6B21\u6539\u4E3A\u53D1\u9001\u56FE\u7247\u7684\u6587\u4EF6\u8DEF\u5F84\uFF08\u5F53\u524D\u6A21\u578B\u80FD\u770B\u56FE\u65F6\u5B83\u4F1A\u7528 read_image \u6253\u5F00\uFF09\uFF1A{paths}",
130
155
  "notice.acpSessionNotRestored": "{agent} \u4E0D\u80FD\u6062\u590D\u4E0A\u4E00\u8F6E\u5BF9\u8BDD\uFF08\u5B83\u7684 ACP \u65E2\u4E0D\u652F\u6301 session/load \u4E5F\u4E0D\u652F\u6301 session/resume\uFF09\uFF0C\u5DF2\u65B0\u5F00\u4E00\u8F6E\uFF1B\u5148\u524D\u7684\u4E0A\u4E0B\u6587\u4E0D\u4F1A\u5E26\u56DE\u3002",
131
156
  "notice.attachmentIssue": "\u9644\u4EF6\u95EE\u9898\uFF1A{detail}",
157
+ "notice.mentionSkipped": "\u8FD9\u4E9B\u8DEF\u5F84\u4E0D\u5728\u9879\u76EE\u91CC\u6216\u5DF2\u4E0D\u5B58\u5728\uFF0C\u6CA1\u6709\u53D1\u7ED9 Agent\uFF1A{paths}",
132
158
  "claude.notFound": "\u627E\u4E0D\u5230 claude\uFF08{detail}\uFF09\u3002\u672C\u63D2\u4EF6\u4E0D\u9644\u5E26 Claude Code\uFF0C\u8BF7\u5148\u5B89\u88C5\uFF1A{hint}",
133
159
  "claude.startFailed": "\u65E0\u6CD5\u542F\u52A8 claude\uFF1A{detail}",
134
160
  "claude.exited": "claude \u8FDB\u7A0B\u610F\u5916\u9000\u51FA\uFF08{code}\uFF09",
@@ -146,6 +172,9 @@ var init_i18n = __esm({
146
172
  "provider.unavailable": "{name} \u672A\u5B89\u88C5\u6216\u4E0D\u53EF\u7528",
147
173
  "provider.unknown": "\u672A\u77E5 Agent\uFF1A{id}",
148
174
  "provider.timeout": "\u68C0\u6D4B\u8D85\u65F6",
175
+ "err.previewUrl": "\u9884\u89C8\u53EA\u652F\u6301\u672C\u673A\u5730\u5740\uFF08localhost\u3001127.0.0.1 \u7B49\uFF09",
176
+ "err.previewDenied": "\u8FD9\u4E2A\u7AEF\u53E3\u4E0D\u80FD\u9884\u89C8\uFF08\u662F Agent \u63A7\u5236\u53F0\u6216 Magpie \u81EA\u5DF1\u7684\u7AEF\u53E3\uFF09",
177
+ "err.previewUnavailable": "\u9884\u89C8\u4E0D\u53EF\u7528",
149
178
  "err.projectMissing": "\u9879\u76EE\u4E0D\u5B58\u5728",
150
179
  "err.threadMissing": "\u5BF9\u8BDD\u4E0D\u5B58\u5728",
151
180
  "err.dirMissing": "\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u6587\u4EF6\u5939",
@@ -235,6 +264,7 @@ var init_i18n = __esm({
235
264
  "notice.acpImageByPathSwitched": "{agent} was started on another model and takes image blocks only for that one, so the image went as its file path (it opens it with read_image when this model sees images): {paths}",
236
265
  "notice.acpSessionNotRestored": "{agent} can't restore the previous conversation (its ACP advertises neither session/load nor session/resume); started a new one \u2014 prior context won't come back.",
237
266
  "notice.attachmentIssue": "Attachment issue: {detail}",
267
+ "notice.mentionSkipped": "Not sent to the Agent (outside the project or gone): {paths}",
238
268
  "claude.notFound": "claude was not found ({detail}). Claude Code is not bundled with this plugin; install it first: {hint}",
239
269
  "claude.startFailed": "Could not start claude: {detail}",
240
270
  "claude.exited": "claude exited unexpectedly ({code})",
@@ -252,6 +282,9 @@ var init_i18n = __esm({
252
282
  "provider.unavailable": "{name} is not installed or not available",
253
283
  "provider.unknown": "Unknown agent: {id}",
254
284
  "provider.timeout": "Detection timed out",
285
+ "err.previewUrl": "Preview only supports local addresses (localhost, 127.0.0.1, \u2026)",
286
+ "err.previewDenied": "This port can't be previewed (it belongs to Agent Console or Magpie)",
287
+ "err.previewUnavailable": "Preview is unavailable",
255
288
  "err.projectMissing": "Project not found",
256
289
  "err.threadMissing": "Conversation not found",
257
290
  "err.dirMissing": "The folder does not exist or is not a folder",
@@ -774,6 +807,7 @@ function pickScript(text) {
774
807
  if (t.startsWith("/error")) return scriptError;
775
808
  if (t.startsWith("/long")) return scriptLong;
776
809
  if (t.startsWith("/exit")) return scriptExit;
810
+ if (t.startsWith("/serve")) return scriptServe;
777
811
  return scriptDefault;
778
812
  }
779
813
  async function sleepAbortable(ms, ctx) {
@@ -789,6 +823,25 @@ function chunkText(s, n) {
789
823
  for (let i = 0; i < s.length; i += n) out.push(s.slice(i, i + n));
790
824
  return out;
791
825
  }
826
+ async function scriptServe(ctx) {
827
+ const emit = (e) => ctx.emit(e);
828
+ const reply = [
829
+ "\u5F00\u53D1\u670D\u52A1\u5668\u5DF2\u7ECF\u542F\u52A8\uFF1Ahttp://localhost:5173/ \uFF0C\u63A5\u53E3\u5728 127.0.0.1:5173/api.json\u3002",
830
+ "",
831
+ "- \u5728\u9884\u89C8\u91CC\u6253\u5F00 [\u9996\u9875](http://localhost:5173/) \u6216 [about](http://localhost:5173/about.html)",
832
+ "- \u6587\u6863\uFF1A[Vite](https://vite.dev/guide/)",
833
+ "",
834
+ "```",
835
+ " VITE v5.4.21 ready in 139 ms",
836
+ " \u279C Local: http://localhost:5173/",
837
+ "```"
838
+ ].join("\n");
839
+ for (const ch of chunkText(reply, 40)) {
840
+ if (await sleepAbortable(20, ctx)) return;
841
+ emit({ type: "assistant_delta", itemId: "sv", text: ch });
842
+ }
843
+ emit({ type: "assistant_message", itemId: "sv", text: reply });
844
+ }
792
845
  function mockReply(ctx) {
793
846
  const base = [
794
847
  "\u597D\u7684\uFF0C\u6211\u5DF2\u7ECF\u6309\u4F60\u7684\u8981\u6C42\u6539\u597D\u4E86\u3002",
@@ -931,21 +984,21 @@ var init_mock = __esm({
931
984
  }
932
985
  }
933
986
  respond(requestId, answer) {
934
- const resolve2 = this.pendingReq.get(requestId);
935
- if (resolve2) {
987
+ const resolve4 = this.pendingReq.get(requestId);
988
+ if (resolve4) {
936
989
  this.pendingReq.delete(requestId);
937
- resolve2(answer);
990
+ resolve4(answer);
938
991
  }
939
992
  }
940
993
  waitFor(requestId) {
941
- return new Promise((resolve2) => {
942
- this.pendingReq.set(requestId, resolve2);
994
+ return new Promise((resolve4) => {
995
+ this.pendingReq.set(requestId, resolve4);
943
996
  });
944
997
  }
945
998
  async interrupt() {
946
999
  this.interruptFlag = true;
947
- for (const [id2, resolve2] of this.pendingReq) {
948
- resolve2({ cancelled: true });
1000
+ for (const [id2, resolve4] of this.pendingReq) {
1001
+ resolve4({ cancelled: true });
949
1002
  this.pendingReq.delete(id2);
950
1003
  }
951
1004
  await sleep(120);
@@ -973,7 +1026,7 @@ var init_mock = __esm({
973
1026
  }
974
1027
  async close() {
975
1028
  this.closed = true;
976
- for (const [, resolve2] of this.pendingReq) resolve2({ cancelled: true });
1029
+ for (const [, resolve4] of this.pendingReq) resolve4({ cancelled: true });
977
1030
  this.pendingReq.clear();
978
1031
  this.q.close();
979
1032
  }
@@ -1037,8 +1090,8 @@ function extraBinDirs({ platform = process.platform, env = process.env, home = h
1037
1090
  return dirs;
1038
1091
  }
1039
1092
  function searchDirs({ platform = process.platform, env = process.env, home = homedir2(), extra = true } = {}) {
1040
- const sep4 = platform === "win32" ? ";" : ":";
1041
- const raw = String(envGet(env, "PATH") ?? "").split(sep4).map((d) => d.trim().replace(/^"(.*)"$/, "$1")).filter(Boolean);
1093
+ const sep5 = platform === "win32" ? ";" : ":";
1094
+ const raw = String(envGet(env, "PATH") ?? "").split(sep5).map((d) => d.trim().replace(/^"(.*)"$/, "$1")).filter(Boolean);
1042
1095
  const all = extra ? [...raw, ...extraBinDirs({ platform, env, home })] : raw;
1043
1096
  const seen = /* @__PURE__ */ new Set();
1044
1097
  const out = [];
@@ -1306,6 +1359,8 @@ var init_bin = __esm({
1306
1359
  });
1307
1360
 
1308
1361
  // daemon/src/agents/kinds.ts
1362
+ import { existsSync as existsSync5 } from "node:fs";
1363
+ import { isAbsolute, resolve as resolve2 } from "node:path";
1309
1364
  function relPath(p, cwd) {
1310
1365
  if (!p) return "";
1311
1366
  const s = String(p);
@@ -1317,6 +1372,15 @@ function relPath(p, cwd) {
1317
1372
  if (a.toLowerCase() === b.toLowerCase()) return ".";
1318
1373
  return s;
1319
1374
  }
1375
+ function targetExists(input, cwd = "") {
1376
+ const p = String(input.file_path ?? input.path ?? "");
1377
+ if (!p) return false;
1378
+ try {
1379
+ return existsSync5(isAbsolute(p) || !cwd ? p : resolve2(cwd, p));
1380
+ } catch {
1381
+ return false;
1382
+ }
1383
+ }
1320
1384
  function callInfo(name, input = {}, cwd = "") {
1321
1385
  const t = String(name || "");
1322
1386
  const f = (kind, title, detail) => ({ kind, title, detail });
@@ -1521,7 +1585,7 @@ function mapToolUse(blk, cwd) {
1521
1585
  const input = rec(blk.input);
1522
1586
  const name = str(blk.name) ?? "tool";
1523
1587
  const id2 = str(blk.id) ?? "";
1524
- const info = callInfo(name, { ...input, _exists: true }, cwd);
1588
+ const info = callInfo(name, { ...input, _exists: name === "Write" ? targetExists(input, cwd) : true }, cwd);
1525
1589
  let detail = info.detail;
1526
1590
  if (info.kind === "edit" && (input.old_string !== void 0 || Array.isArray(input.edits))) {
1527
1591
  detail = editDetail({ path: info.detail.path, old: input.old_string, new: input.new_string, edits: input.edits });
@@ -1573,7 +1637,7 @@ __export(attachmentRefs_exports, {
1573
1637
  resolveAttachmentRefs: () => resolveAttachmentRefs,
1574
1638
  resolveImagePath: () => resolveImagePath
1575
1639
  });
1576
- import { existsSync as existsSync5 } from "node:fs";
1640
+ import { existsSync as existsSync6 } from "node:fs";
1577
1641
  function bindAttachmentStore(store) {
1578
1642
  globalAttachmentStore = store;
1579
1643
  }
@@ -1596,7 +1660,7 @@ async function resolveAttachmentRefs(store, refs) {
1596
1660
  out.push({ path: viaGlobal, mime: a.mime });
1597
1661
  continue;
1598
1662
  }
1599
- if (existsSync5(a.path)) {
1663
+ if (existsSync6(a.path)) {
1600
1664
  out.push(a);
1601
1665
  continue;
1602
1666
  }
@@ -1605,7 +1669,7 @@ async function resolveAttachmentRefs(store, refs) {
1605
1669
  return out;
1606
1670
  }
1607
1671
  async function resolveImagePath(ref) {
1608
- if (existsSync5(ref)) return ref;
1672
+ if (existsSync6(ref)) return ref;
1609
1673
  const p = await lookupAttachmentPath(ref);
1610
1674
  if (p) return p;
1611
1675
  throw new Error(tr("attachment.missing", { ref }));
@@ -1768,15 +1832,15 @@ var init_cli = __esm({
1768
1832
  });
1769
1833
  this.child.stdin?.on("error", () => {
1770
1834
  });
1771
- this.exited = new Promise((resolve2) => {
1835
+ this.exited = new Promise((resolve4) => {
1772
1836
  this.child.on("error", (e) => {
1773
1837
  this.spawnError = e;
1774
1838
  this.finish();
1775
- resolve2({ code: null, signal: null });
1839
+ resolve4({ code: null, signal: null });
1776
1840
  });
1777
1841
  this.child.on("close", (code, signal) => {
1778
1842
  this.finish();
1779
- resolve2({ code, signal });
1843
+ resolve4({ code, signal });
1780
1844
  });
1781
1845
  });
1782
1846
  this.initialized = this.request({ subtype: "initialize" }, 6e4).catch(() => null);
@@ -1893,8 +1957,8 @@ var init_cli = __esm({
1893
1957
  request(request, timeoutMs = 1e4) {
1894
1958
  if (this.dead) return Promise.reject(new Error("claude is not running"));
1895
1959
  const id2 = `ac_${++this.seq}_${Math.random().toString(36).slice(2, 8)}`;
1896
- return new Promise((resolve2, reject) => {
1897
- const p = { resolve: resolve2, reject };
1960
+ return new Promise((resolve4, reject) => {
1961
+ const p = { resolve: resolve4, reject };
1898
1962
  p.timer = setTimeout(() => {
1899
1963
  if (this.pending.delete(id2)) reject(new Error(`claude did not answer ${String(request.subtype)} in ${Math.round(timeoutMs / 1e3)} s`));
1900
1964
  }, timeoutMs);
@@ -2066,7 +2130,7 @@ var init_session = __esm({
2066
2130
  if (toolName === "AskUserQuestion" && Array.isArray(input.questions)) {
2067
2131
  return this.handleQuestion(requestId, input, signal);
2068
2132
  }
2069
- const info = callInfo(toolName, { ...input, _exists: true }, this.o.cwd);
2133
+ const info = callInfo(toolName, { ...input, _exists: toolName === "Write" ? targetExists(input, this.o.cwd) : true }, this.o.cwd);
2070
2134
  let detail = { ...info.detail, toolName };
2071
2135
  if (info.kind === "edit" && (input.old_string !== void 0 || Array.isArray(input.edits))) {
2072
2136
  detail = { ...editDetail({ path: info.detail.path, old: input.old_string, new: input.new_string, edits: input.edits }), toolName };
@@ -2083,12 +2147,12 @@ var init_session = __esm({
2083
2147
  { id: "deny", label: tr("perm.deny"), kind: "reject_once" }
2084
2148
  ]
2085
2149
  });
2086
- return new Promise((resolve2) => {
2087
- this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve2(this.toPermissionDecision(a, input)) });
2150
+ return new Promise((resolve4) => {
2151
+ this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve4(this.toPermissionDecision(a, input)) });
2088
2152
  signal.addEventListener("abort", () => {
2089
2153
  if (this.pending.delete(requestId)) {
2090
2154
  this.emit({ type: "permission_resolved", requestId, cancelled: true });
2091
- resolve2({ behavior: "deny", message: "Cancelled by the user", interrupt: true });
2155
+ resolve4({ behavior: "deny", message: "Cancelled by the user", interrupt: true });
2092
2156
  }
2093
2157
  }, { once: true });
2094
2158
  });
@@ -2115,12 +2179,12 @@ var init_session = __esm({
2115
2179
  { id: "reject", label: tr("perm.deny"), kind: "reject_once" }
2116
2180
  ]
2117
2181
  });
2118
- const answer = await new Promise((resolve2) => {
2119
- this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve2(a) });
2182
+ const answer = await new Promise((resolve4) => {
2183
+ this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve4(a) });
2120
2184
  signal.addEventListener("abort", () => {
2121
2185
  if (this.pending.delete(requestId)) {
2122
2186
  this.emit({ type: "permission_resolved", requestId, cancelled: true });
2123
- resolve2({ cancelled: true });
2187
+ resolve4({ cancelled: true });
2124
2188
  }
2125
2189
  }, { once: true });
2126
2190
  });
@@ -2138,12 +2202,12 @@ var init_session = __esm({
2138
2202
  }
2139
2203
  async handleQuestion(requestId, input, signal) {
2140
2204
  this.emit({ type: "question", requestId, questions: input.questions });
2141
- const answer = await new Promise((resolve2) => {
2142
- this.pending.set(requestId, { kind: "question", resolve: (a) => resolve2(a) });
2205
+ const answer = await new Promise((resolve4) => {
2206
+ this.pending.set(requestId, { kind: "question", resolve: (a) => resolve4(a) });
2143
2207
  signal.addEventListener("abort", () => {
2144
2208
  if (this.pending.delete(requestId)) {
2145
2209
  this.emit({ type: "permission_resolved", requestId, cancelled: true });
2146
- resolve2({ cancelled: true });
2210
+ resolve4({ cancelled: true });
2147
2211
  }
2148
2212
  }, { once: true });
2149
2213
  });
@@ -2265,7 +2329,7 @@ var init_models = __esm({
2265
2329
 
2266
2330
  // daemon/src/history/import.ts
2267
2331
  import { readdir, readFile as readFile5, stat as stat4 } from "node:fs/promises";
2268
- import { existsSync as existsSync6 } from "node:fs";
2332
+ import { existsSync as existsSync7 } from "node:fs";
2269
2333
  import { join as join6 } from "node:path";
2270
2334
  import { homedir as homedir3 } from "node:os";
2271
2335
  function parseLine(line) {
@@ -2332,7 +2396,7 @@ async function readClaudeHistory(sessionId, cwd) {
2332
2396
  const dirs = await readdir(CLAUDE_ROOT).catch(() => []);
2333
2397
  for (const d of dirs) {
2334
2398
  const path3 = join6(CLAUDE_ROOT, d, `${sessionId}.jsonl`);
2335
- if (!existsSync6(path3)) continue;
2399
+ if (!existsSync7(path3)) continue;
2336
2400
  const msgs = [];
2337
2401
  let matched = false;
2338
2402
  for await (const line of linesOf(path3)) {
@@ -2387,14 +2451,14 @@ function runVersion(r, args, timeoutMs = 5e3) {
2387
2451
  });
2388
2452
  }
2389
2453
  function createClaudeProvider(opts = {}) {
2390
- const resolve2 = opts.resolve ?? resolveBin;
2454
+ const resolve4 = opts.resolve ?? resolveBin;
2391
2455
  const resolveCustom = opts.resolve ? (l) => opts.resolve(l.cmd) : resolveLaunch;
2392
2456
  return {
2393
2457
  id: "claude",
2394
2458
  name: "Claude Code",
2395
2459
  capabilities: claudeCapabilities,
2396
2460
  async detect(launch) {
2397
- const bin = launch ? await resolveCustom(launch) : await resolve2("claude");
2461
+ const bin = launch ? await resolveCustom(launch) : await resolve4("claude");
2398
2462
  if (!bin.ok) return { available: false, reason: bin.reason, installHint: INSTALL_HINT };
2399
2463
  try {
2400
2464
  const version = await runVersion(bin, [...launch?.prefixArgs ?? [], "--version"]);
@@ -2407,7 +2471,7 @@ function createClaudeProvider(opts = {}) {
2407
2471
  return listClaudeModels();
2408
2472
  },
2409
2473
  async createSession(o) {
2410
- const bin = o.launch ? await resolveCustom(o.launch) : await resolve2("claude");
2474
+ const bin = o.launch ? await resolveCustom(o.launch) : await resolve4("claude");
2411
2475
  if (!bin.ok) throw new Error(tr("claude.notFound", { detail: bin.reason, hint: INSTALL_HINT }));
2412
2476
  return new ClaudeSession({
2413
2477
  threadId: o.threadId,
@@ -3423,8 +3487,8 @@ var init_session2 = __esm({
3423
3487
  }
3424
3488
  }
3425
3489
  waitFor(requestId) {
3426
- return new Promise((resolve2) => {
3427
- this.pending.set(requestId, { kind: "permission", resolve: resolve2 });
3490
+ return new Promise((resolve4) => {
3491
+ this.pending.set(requestId, { kind: "permission", resolve: resolve4 });
3428
3492
  });
3429
3493
  }
3430
3494
  respond(requestId, answer) {
@@ -3943,7 +4007,7 @@ var init_sessionCaps = __esm({
3943
4007
  import { readFile as readFile10, stat as stat7 } from "node:fs/promises";
3944
4008
  import { basename as basename4 } from "node:path";
3945
4009
  import { pathToFileURL } from "node:url";
3946
- async function buildAcpPrompt(text, attachments, caps) {
4010
+ async function buildAcpPrompt(text, attachments, caps, mentions = []) {
3947
4011
  const media = [];
3948
4012
  const links = [];
3949
4013
  const pathLines = [];
@@ -4006,6 +4070,9 @@ async function buildAcpPrompt(text, attachments, caps) {
4006
4070
  const blocks = [];
4007
4071
  if (fullText) blocks.push({ type: "text", text: fullText });
4008
4072
  blocks.push(...media, ...links);
4073
+ for (const m of mentions) {
4074
+ blocks.push({ type: "resource_link", uri: pathToFileURL(m.path).href, name: basename4(m.path) || m.path });
4075
+ }
4009
4076
  return { blocks, imagesByPath, warnings };
4010
4077
  }
4011
4078
  var MAX_INLINE_MEDIA_BYTES, MAX_INLINE_TOTAL_BYTES, MAX_EMBED_TEXT_BYTES, isText;
@@ -4309,8 +4376,8 @@ var init_session3 = __esm({
4309
4376
  await this.applyCursorAutoReviewConfig(false);
4310
4377
  }
4311
4378
  waitPerm(requestId) {
4312
- return new Promise((resolve2) => {
4313
- this.permPending.set(requestId, resolve2);
4379
+ return new Promise((resolve4) => {
4380
+ this.permPending.set(requestId, resolve4);
4314
4381
  });
4315
4382
  }
4316
4383
  respond(requestId, answer) {
@@ -4347,7 +4414,7 @@ var init_session3 = __esm({
4347
4414
  this.q.push({ type: "turn_ended", turnId: this.turnId, status: "completed" });
4348
4415
  return;
4349
4416
  }
4350
- const built = await buildAcpPrompt(text, attachments, this.promptCaps);
4417
+ const built = await buildAcpPrompt(text, attachments, this.promptCaps, input.mentions ?? []);
4351
4418
  const agent = this.o.providerName ?? this.o.binName;
4352
4419
  if (built.imagesByPath.length && !this.promptCaps.image) {
4353
4420
  const switched = this.o.imagesFollowStartModel && this.initCaps.image && this.o.model !== this.startModel;
@@ -4966,13 +5033,13 @@ var init_pickFolderWin = __esm({
4966
5033
 
4967
5034
  // daemon/src/util/pickFolder.ts
4968
5035
  import { execFile as execFile2 } from "node:child_process";
4969
- import { existsSync as existsSync7, statSync as statSync3 } from "node:fs";
5036
+ import { existsSync as existsSync8, statSync as statSync3 } from "node:fs";
4970
5037
  function appleScriptString(s) {
4971
5038
  return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
4972
5039
  }
4973
5040
  function isDir(p) {
4974
5041
  try {
4975
- return !!p && existsSync7(p) && statSync3(p).isDirectory();
5042
+ return !!p && existsSync8(p) && statSync3(p).isDirectory();
4976
5043
  } catch {
4977
5044
  return false;
4978
5045
  }
@@ -5222,13 +5289,869 @@ var init_modes = __esm({
5222
5289
  }
5223
5290
  });
5224
5291
 
5292
+ // daemon/src/util/mentions.ts
5293
+ import { realpath, stat as stat8 } from "node:fs/promises";
5294
+ import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep2, win32 as win322, posix as posix2 } from "node:path";
5295
+ function insideRoot(root, child, platform = process.platform) {
5296
+ const p = platform === "win32" ? win322 : posix2;
5297
+ const norm = (s) => platform === "win32" ? p.resolve(s).toLowerCase() : p.resolve(s);
5298
+ const r = p.relative(norm(root), norm(child));
5299
+ return r === "" || !r.startsWith("..") && !p.isAbsolute(r);
5300
+ }
5301
+ async function resolveMentions(projectPath, refs) {
5302
+ const mentions = [];
5303
+ const warnings = [];
5304
+ const seen = /* @__PURE__ */ new Set();
5305
+ const shown = resolve3(projectPath);
5306
+ let realRoot;
5307
+ try {
5308
+ realRoot = await realpath(shown);
5309
+ } catch {
5310
+ return { mentions, warnings: refs.map((r) => r.rel) };
5311
+ }
5312
+ for (const ref of refs.slice(0, MAX_MENTIONS)) {
5313
+ const rel = String(ref.rel ?? "").replace(/\\/g, "/");
5314
+ if (!rel || rel.includes("\0") || isAbsolute2(rel) || /^[a-zA-Z]:/.test(rel)) {
5315
+ warnings.push(rel || "(empty)");
5316
+ continue;
5317
+ }
5318
+ const abs = resolve3(shown, rel === "." ? "" : rel);
5319
+ if (!insideRoot(shown, abs)) {
5320
+ warnings.push(rel);
5321
+ continue;
5322
+ }
5323
+ let real;
5324
+ let isDir2;
5325
+ try {
5326
+ real = await realpath(abs);
5327
+ isDir2 = (await stat8(real)).isDirectory();
5328
+ } catch {
5329
+ warnings.push(rel);
5330
+ continue;
5331
+ }
5332
+ if (!insideRoot(realRoot, real)) {
5333
+ warnings.push(rel);
5334
+ continue;
5335
+ }
5336
+ if (seen.has(abs)) continue;
5337
+ seen.add(abs);
5338
+ mentions.push({ path: abs, isDir: isDir2 });
5339
+ }
5340
+ return { mentions, warnings };
5341
+ }
5342
+ function mentionSection(mentions) {
5343
+ if (!mentions.length) return "";
5344
+ const lines = mentions.map((m) => {
5345
+ const shown = m.isDir && !m.path.endsWith(sep2) && !m.path.endsWith("/") ? m.path + (m.path.includes("\\") && !m.path.includes("/") ? "\\" : "/") : m.path;
5346
+ const quoted = /\s|`/.test(shown) ? "`" + shown.replace(/`/g, "'") + "`" : shown;
5347
+ return `- ${quoted}${m.isDir ? " (folder)" : ""}`;
5348
+ });
5349
+ return `Referenced files (read them as needed):
5350
+ ${lines.join("\n")}`;
5351
+ }
5352
+ function withMentions(text, mentions) {
5353
+ const sec = mentionSection(mentions);
5354
+ if (!sec) return text;
5355
+ return text.trim() ? `${text}
5356
+
5357
+ ${sec}` : sec;
5358
+ }
5359
+ var MAX_MENTIONS;
5360
+ var init_mentions = __esm({
5361
+ "daemon/src/util/mentions.ts"() {
5362
+ MAX_MENTIONS = 50;
5363
+ }
5364
+ });
5365
+
5366
+ // daemon/src/preview/url.ts
5367
+ function isLoopbackHost(host) {
5368
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
5369
+ if (h === "localhost" || h.endsWith(".localhost")) return /^[a-z0-9.-]+$/.test(h) && !h.includes("..");
5370
+ if (h === "0.0.0.0" || h === "::1" || h === "::") return true;
5371
+ const m = /^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
5372
+ return !!m && m.slice(1).every((x) => Number(x) <= 255);
5373
+ }
5374
+ function connectHosts(h) {
5375
+ if (h === "localhost" || h.endsWith(".localhost")) return ["127.0.0.1", "::1"];
5376
+ if (h === "0.0.0.0") return ["127.0.0.1"];
5377
+ if (h === "::1") return ["::1"];
5378
+ if (h === "::") return ["::1", "127.0.0.1"];
5379
+ return [h];
5380
+ }
5381
+ function withScheme(raw) {
5382
+ const s = raw.trim();
5383
+ if (/^\d{1,5}$/.test(s)) return `http://localhost:${s}/`;
5384
+ if (/^:\d{1,5}(?:[/?#].*)?$/.test(s)) return `http://localhost${s}`;
5385
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return s;
5386
+ if (/^[a-z][a-z0-9+.-]*:(?!\d)/i.test(s)) return s;
5387
+ return `http://${s}`;
5388
+ }
5389
+ function checkPreviewUrl(raw) {
5390
+ if (typeof raw !== "string" || !raw.trim()) return { ok: false, reason: "empty" };
5391
+ if (raw.length > MAX_PREVIEW_URL) return { ok: false, reason: "too_long" };
5392
+ let u;
5393
+ try {
5394
+ u = new URL(withScheme(raw));
5395
+ } catch {
5396
+ return { ok: false, reason: "bad_url" };
5397
+ }
5398
+ if (u.protocol !== "http:" && u.protocol !== "https:") return { ok: false, reason: "scheme" };
5399
+ if (u.username || u.password) return { ok: false, reason: "userinfo" };
5400
+ const hostname = u.hostname.toLowerCase().replace(/^\[|\]$/g, "");
5401
+ if (!isLoopbackHost(hostname)) return { ok: false, reason: "not_local" };
5402
+ const secure = u.protocol === "https:";
5403
+ const port = u.port ? Number(u.port) : secure ? 443 : 80;
5404
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return { ok: false, reason: "port" };
5405
+ const href = u.href;
5406
+ if (href.length > MAX_PREVIEW_URL) return { ok: false, reason: "too_long" };
5407
+ const hostForUrl = hostname.includes(":") ? `[${hostname}]` : hostname;
5408
+ return {
5409
+ ok: true,
5410
+ target: {
5411
+ href,
5412
+ origin: `${u.protocol}//${hostForUrl}:${port}`,
5413
+ path: `${u.pathname}${u.search}${u.hash}`,
5414
+ secure,
5415
+ hostname,
5416
+ port,
5417
+ connect: connectHosts(hostname),
5418
+ hostHeader: `${hostForUrl}:${port}`
5419
+ }
5420
+ };
5421
+ }
5422
+ function deniedPort(port, denied) {
5423
+ for (const d of denied) if (d === port) return true;
5424
+ return false;
5425
+ }
5426
+ function displayOrigin(origin) {
5427
+ return origin.replace(/^http:\/\/(.+):80$/, "http://$1").replace(/^https:\/\/(.+):443$/, "https://$1");
5428
+ }
5429
+ function externalHref(raw) {
5430
+ if (typeof raw !== "string" || !raw || raw.length > MAX_PREVIEW_URL) return null;
5431
+ let u;
5432
+ try {
5433
+ u = new URL(raw);
5434
+ } catch {
5435
+ return null;
5436
+ }
5437
+ if (u.protocol !== "http:" && u.protocol !== "https:") return null;
5438
+ if (!u.hostname) return null;
5439
+ const href = u.href;
5440
+ if (/[\s"'<>`\\^{|}]/.test(href) || href.length > MAX_PREVIEW_URL) return null;
5441
+ return href;
5442
+ }
5443
+ var MAX_PREVIEW_URL;
5444
+ var init_url = __esm({
5445
+ "daemon/src/preview/url.ts"() {
5446
+ MAX_PREVIEW_URL = 2048;
5447
+ }
5448
+ });
5449
+
5450
+ // daemon/src/util/openExternal.ts
5451
+ import { execFile as execFile3 } from "node:child_process";
5452
+ function openerFor(platform, href) {
5453
+ if (platform === "win32") return { cmd: "rundll32.exe", args: ["url.dll,FileProtocolHandler", href] };
5454
+ if (platform === "darwin") return { cmd: "open", args: [href] };
5455
+ return { cmd: "xdg-open", args: [href] };
5456
+ }
5457
+ async function openExternal(raw, opts = {}) {
5458
+ const href = externalHref(raw);
5459
+ if (!href) return { ok: false, reason: "bad_url" };
5460
+ const { cmd, args } = openerFor(opts.platform ?? process.platform, href);
5461
+ const run = opts.exec ?? execFile3;
5462
+ return await new Promise((resolve4) => {
5463
+ let settled = false;
5464
+ const done = (v) => {
5465
+ if (!settled) {
5466
+ settled = true;
5467
+ resolve4(v);
5468
+ }
5469
+ };
5470
+ try {
5471
+ const child = run(cmd, args, { windowsHide: true, timeout: 15e3 }, (err) => {
5472
+ done(err ? { ok: false, href, reason: "opener_failed" } : { ok: true, href });
5473
+ });
5474
+ child.on("error", () => done({ ok: false, href, reason: "no_opener" }));
5475
+ } catch {
5476
+ done({ ok: false, href, reason: "no_opener" });
5477
+ }
5478
+ setTimeout(() => done({ ok: true, href }), opts.waitMs ?? 4e3).unref?.();
5479
+ });
5480
+ }
5481
+ var init_openExternal = __esm({
5482
+ "daemon/src/util/openExternal.ts"() {
5483
+ init_url();
5484
+ }
5485
+ });
5486
+
5487
+ // daemon/src/preview/inject.ts
5488
+ function injectScriptTag(html) {
5489
+ const tag = `<script src="${PREVIEW_SCRIPT_PATH}"></script>`;
5490
+ const head = /<head(\s[^>]*)?>/i.exec(html);
5491
+ if (head) return html.slice(0, head.index + head[0].length) + tag + html.slice(head.index + head[0].length);
5492
+ const root = /<html(\s[^>]*)?>/i.exec(html);
5493
+ if (root) return html.slice(0, root.index + root[0].length) + tag + html.slice(root.index + root[0].length);
5494
+ const doctype = /^\s*<!doctype[^>]*>/i.exec(html);
5495
+ if (doctype) return html.slice(0, doctype[0].length) + tag + html.slice(doctype[0].length);
5496
+ return tag + html;
5497
+ }
5498
+ var PREVIEW_SCRIPT_PATH, PREVIEW_SCRIPT;
5499
+ var init_inject = __esm({
5500
+ "daemon/src/preview/inject.ts"() {
5501
+ PREVIEW_SCRIPT_PATH = "/__agent_console__/preview.js";
5502
+ PREVIEW_SCRIPT = `(function(){
5503
+ "use strict";
5504
+ if (window.__acPreview) return; window.__acPreview = 1;
5505
+ function mem(){ var m = new Map(); var s = {
5506
+ getItem: function(k){ k = String(k); return m.has(k) ? m.get(k) : null; },
5507
+ setItem: function(k, v){ m.set(String(k), String(v)); },
5508
+ removeItem: function(k){ m.delete(String(k)); },
5509
+ clear: function(){ m.clear(); },
5510
+ key: function(i){ var a = Array.from(m.keys()); return i >= 0 && i < a.length ? a[i] : null; }
5511
+ }; Object.defineProperty(s, "length", { get: function(){ return m.size; } }); return s; }
5512
+ ["localStorage", "sessionStorage"].forEach(function(n){
5513
+ var ok = true; try { var st = window[n]; st.getItem("__ac"); } catch (e) { ok = false; }
5514
+ if (!ok) { try { Object.defineProperty(window, n, { value: mem(), configurable: true }); } catch (e) {} }
5515
+ });
5516
+ try { void document.cookie; } catch (e) {
5517
+ var jar = {};
5518
+ try { Object.defineProperty(document, "cookie", { configurable: true,
5519
+ get: function(){ return Object.keys(jar).map(function(k){ return k + "=" + jar[k]; }).join("; "); },
5520
+ set: function(v){ var p = String(v).split(";")[0]; var i = p.indexOf("="); if (i > 0) jar[p.slice(0, i).trim()] = p.slice(i + 1).trim(); }
5521
+ }); } catch (e2) {}
5522
+ }
5523
+ var parentWin = window.parent;
5524
+ if (!parentWin || parentWin === window) return;
5525
+ var last = "";
5526
+ function report(){ try {
5527
+ var msg = { ac_preview: 1, href: String(location.href), title: String(document.title || "") };
5528
+ var key = msg.href + "\\n" + msg.title; if (key === last) return; last = key;
5529
+ parentWin.postMessage(msg, "*");
5530
+ } catch (e) {} }
5531
+ ["pushState", "replaceState"].forEach(function(k){
5532
+ var orig = history[k]; if (typeof orig !== "function") return;
5533
+ history[k] = function(){ var r = orig.apply(this, arguments); setTimeout(report, 0); return r; };
5534
+ });
5535
+ window.addEventListener("popstate", report);
5536
+ window.addEventListener("hashchange", report);
5537
+ window.addEventListener("load", report);
5538
+ document.addEventListener("DOMContentLoaded", report);
5539
+ setInterval(report, 1000);
5540
+ window.addEventListener("message", function(e){
5541
+ if (e.source !== parentWin) return;
5542
+ var d = e.data; if (!d || typeof d !== "object") return;
5543
+ var c = d.ac_preview_cmd;
5544
+ if (c === "back") history.back();
5545
+ else if (c === "forward") history.forward();
5546
+ else if (c === "reload") location.reload();
5547
+ else if (c === "ping") { last = ""; report(); }
5548
+ });
5549
+ report();
5550
+ })();
5551
+ `;
5552
+ }
5553
+ });
5554
+
5555
+ // daemon/src/preview/proxy.ts
5556
+ import { createServer as createServer2, request as httpRequest } from "node:http";
5557
+ import { request as httpsRequest } from "node:https";
5558
+ import { connect as netConnect, isIP } from "node:net";
5559
+ import { connect as tlsConnect } from "node:tls";
5560
+ import { randomBytes as randomBytes3 } from "node:crypto";
5561
+ function esc(s) {
5562
+ return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
5563
+ }
5564
+ function rewriteRequestHeaders(inH, t, proxyOrigin, upgrade) {
5565
+ const out = {};
5566
+ const targetOrigin = displayOrigin(t.origin);
5567
+ for (const [k, v] of Object.entries(inH)) {
5568
+ if (v === void 0) continue;
5569
+ const key = k.toLowerCase();
5570
+ if (!upgrade && HOP.has(key)) continue;
5571
+ if (upgrade && (key === "proxy-connection" || key === "proxy-authorization" || key === "keep-alive" || key === "te" || key === "trailer" || key === "transfer-encoding")) continue;
5572
+ if (key === "cookie" || key === "host" || key === "origin" || key === "referer" || key === "forwarded" || key.startsWith("x-forwarded-")) continue;
5573
+ out[key] = v;
5574
+ }
5575
+ out.host = t.hostHeader.replace(/:(80|443)$/, (m) => m === ":80" && !t.secure || m === ":443" && t.secure ? "" : m);
5576
+ if (inH.origin !== void 0) out.origin = targetOrigin;
5577
+ const ref = inH.referer;
5578
+ if (typeof ref === "string" && (ref === proxyOrigin || ref.startsWith(proxyOrigin + "/"))) out.referer = targetOrigin + ref.slice(proxyOrigin.length);
5579
+ if (inH["sec-fetch-site"] !== void 0) out["sec-fetch-site"] = "same-origin";
5580
+ if (!upgrade) out["accept-encoding"] = "identity";
5581
+ return out;
5582
+ }
5583
+ function rewriteResponseHeaders(inH, t, proxyOrigin) {
5584
+ const out = {};
5585
+ for (const [k, v] of Object.entries(inH)) {
5586
+ if (v === void 0) continue;
5587
+ const key = k.toLowerCase();
5588
+ if (HOP.has(key)) continue;
5589
+ if (key === "x-frame-options" || key === "set-cookie" || key === "referrer-policy" || key.startsWith("access-control-allow-")) continue;
5590
+ if (key === "content-security-policy" || key === "content-security-policy-report-only") {
5591
+ const kept = (Array.isArray(v) ? v : [v]).map(stripFrameAncestors).filter(Boolean);
5592
+ if (kept.length) out[key] = kept.join(", ");
5593
+ continue;
5594
+ }
5595
+ if (key === "location" && typeof v === "string") {
5596
+ out[key] = rewriteLocation(v, t, proxyOrigin);
5597
+ continue;
5598
+ }
5599
+ out[key] = v;
5600
+ }
5601
+ out["access-control-allow-origin"] = "null";
5602
+ out["referrer-policy"] = "no-referrer";
5603
+ const vary = typeof out.vary === "string" ? out.vary : "";
5604
+ out.vary = vary ? /\borigin\b/i.test(vary) ? vary : `${vary}, Origin` : "Origin";
5605
+ return out;
5606
+ }
5607
+ function stripFrameAncestors(csp) {
5608
+ return csp.split(";").map((d) => d.trim()).filter((d) => d && !/^frame-ancestors(\s|$)/i.test(d)).join("; ");
5609
+ }
5610
+ function rewriteLocation(loc, t, proxyOrigin) {
5611
+ let u;
5612
+ try {
5613
+ u = new URL(loc, t.href);
5614
+ } catch {
5615
+ return loc;
5616
+ }
5617
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(loc) && !loc.startsWith("//")) return loc;
5618
+ const port = u.port ? Number(u.port) : u.protocol === "https:" ? 443 : 80;
5619
+ const host = u.hostname.toLowerCase().replace(/^\[|\]$/g, "");
5620
+ if (u.protocol === (t.secure ? "https:" : "http:") && host === t.hostname && port === t.port) {
5621
+ return proxyOrigin + u.pathname + u.search + u.hash;
5622
+ }
5623
+ return loc;
5624
+ }
5625
+ function looksLikeMagpie(h) {
5626
+ return Object.keys(h).some((k) => k.toLowerCase().startsWith("x-magpie-"));
5627
+ }
5628
+ function magpieIndex(body) {
5629
+ try {
5630
+ const j = JSON.parse(body);
5631
+ return !!j && typeof j === "object" && j.name === "magpie" && (Array.isArray(j.apis) || typeof j.version === "string");
5632
+ } catch {
5633
+ return false;
5634
+ }
5635
+ }
5636
+ async function identify(t, addrs, timeoutMs) {
5637
+ for (const addr of addrs) {
5638
+ const r = await new Promise((resolve4) => {
5639
+ let settled = false;
5640
+ const done = (v) => {
5641
+ if (!settled) {
5642
+ settled = true;
5643
+ clearTimeout(timer);
5644
+ resolve4(v);
5645
+ }
5646
+ };
5647
+ const reqFn = t.secure ? httpsRequest : httpRequest;
5648
+ const q = reqFn({
5649
+ host: addr,
5650
+ port: t.port,
5651
+ method: "GET",
5652
+ path: "/",
5653
+ headers: { host: t.hostHeader, accept: "application/json", "accept-encoding": "identity", "user-agent": "agent-console-preview-check" },
5654
+ agent: false,
5655
+ ...t.secure ? { rejectUnauthorized: false, servername: isIP(t.hostname) ? void 0 : t.hostname } : {}
5656
+ });
5657
+ const timer = setTimeout(() => {
5658
+ q.destroy();
5659
+ done("none");
5660
+ }, timeoutMs);
5661
+ q.on("error", () => done("none"));
5662
+ q.on("response", (res) => {
5663
+ if (looksLikeMagpie(res.headers)) {
5664
+ res.destroy();
5665
+ return done("magpie");
5666
+ }
5667
+ const ct = String(res.headers["content-type"] ?? "").toLowerCase();
5668
+ if (!ct.includes("json")) {
5669
+ res.destroy();
5670
+ return done("other");
5671
+ }
5672
+ const chunks = [];
5673
+ let size = 0;
5674
+ res.on("data", (c) => {
5675
+ size += c.length;
5676
+ if (size > 256 * 1024) {
5677
+ res.destroy();
5678
+ done("other");
5679
+ return;
5680
+ }
5681
+ chunks.push(c);
5682
+ });
5683
+ res.on("end", () => done(magpieIndex(Buffer.concat(chunks).toString("utf8")) ? "magpie" : "other"));
5684
+ res.on("error", () => done("none"));
5685
+ });
5686
+ q.end();
5687
+ });
5688
+ if (r !== "none") return r;
5689
+ }
5690
+ return "none";
5691
+ }
5692
+ var PreviewError, HOP, RETRY_CODES, IDENTITY_TTL, PREVIEW_PING_PATH, PreviewProxy;
5693
+ var init_proxy = __esm({
5694
+ "daemon/src/preview/proxy.ts"() {
5695
+ init_security();
5696
+ init_url();
5697
+ init_inject();
5698
+ PreviewError = class extends Error {
5699
+ code;
5700
+ constructor(code, message) {
5701
+ super(message);
5702
+ this.code = code;
5703
+ }
5704
+ };
5705
+ HOP = /* @__PURE__ */ new Set(["connection", "keep-alive", "proxy-connection", "proxy-authorization", "proxy-authenticate", "te", "trailer", "transfer-encoding", "upgrade"]);
5706
+ RETRY_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "EADDRNOTAVAIL", "EAFNOSUPPORT", "ENETUNREACH", "EHOSTUNREACH", "ECONNRESET", "ETIMEDOUT"]);
5707
+ IDENTITY_TTL = 60 * 1e3;
5708
+ PREVIEW_PING_PATH = "/__agent_console__/ping";
5709
+ PreviewProxy = class {
5710
+ constructor(opts) {
5711
+ this.opts = opts;
5712
+ this.idleMs = opts.idleMs ?? 10 * 60 * 1e3;
5713
+ this.maxTargets = opts.maxTargets ?? 8;
5714
+ this.maxHtml = opts.maxHtmlBytes ?? 8 * 1024 * 1024;
5715
+ this.now = opts.now ?? Date.now;
5716
+ this.identityTimeout = opts.identityTimeoutMs ?? 30 * 1e3;
5717
+ }
5718
+ opts;
5719
+ byOrigin = /* @__PURE__ */ new Map();
5720
+ opening = /* @__PURE__ */ new Map();
5721
+ sweepTimer = null;
5722
+ idleMs;
5723
+ maxTargets;
5724
+ maxHtml;
5725
+ now;
5726
+ identityTimeout;
5727
+ /** Ports of the listeners themselves (a preview of a preview is refused) */
5728
+ ports() {
5729
+ return [...this.byOrigin.values()].map((l) => l.port);
5730
+ }
5731
+ get size() {
5732
+ return this.byOrigin.size;
5733
+ }
5734
+ async open(raw) {
5735
+ const c = checkPreviewUrl(raw);
5736
+ if (!c.ok) throw new PreviewError(c.reason, `preview url refused: ${c.reason}`);
5737
+ const t = c.target;
5738
+ if (deniedPort(t.port, [...this.opts.deniedPorts(), ...this.ports()])) {
5739
+ throw new PreviewError("denied_port", `port ${t.port} can't be previewed`);
5740
+ }
5741
+ let l = this.byOrigin.get(t.origin);
5742
+ if (!l) {
5743
+ const who = await identify(t, t.connect, Math.min(3e3, this.identityTimeout));
5744
+ if (who === "magpie") {
5745
+ this.opts.log?.("warn", `preview refused: ${t.origin} answers like Magpie's gateway`);
5746
+ throw new PreviewError("denied_port", `${t.origin} is Magpie's gateway`);
5747
+ }
5748
+ let p = this.opening.get(t.origin);
5749
+ if (!p) {
5750
+ p = this.listen(t).finally(() => this.opening.delete(t.origin));
5751
+ this.opening.set(t.origin, p);
5752
+ }
5753
+ l = await p;
5754
+ }
5755
+ l.lastUsed = this.now();
5756
+ l.nonce = randomBytes3(16).toString("hex");
5757
+ l.canaryAuthority = `acprobe${randomBytes3(16).toString("hex")}.localhost:${l.port}`;
5758
+ l.canaryNonce = randomBytes3(16).toString("hex");
5759
+ this.ensureSweep();
5760
+ return {
5761
+ proxyOrigin: l.proxyOrigin,
5762
+ target: t.origin,
5763
+ url: t.href,
5764
+ path: t.path,
5765
+ proxyHref: l.proxyOrigin + t.path,
5766
+ nonce: l.nonce,
5767
+ canaryOrigin: `http://${l.canaryAuthority}`,
5768
+ canaryNonce: l.canaryNonce
5769
+ };
5770
+ }
5771
+ /** Close the listener for this target (any URL on it, or its origin) */
5772
+ release(raw) {
5773
+ const c = checkPreviewUrl(raw);
5774
+ if (!c.ok) return false;
5775
+ const l = this.byOrigin.get(c.target.origin);
5776
+ if (!l) return false;
5777
+ this.close(l);
5778
+ return true;
5779
+ }
5780
+ closeAll() {
5781
+ for (const l of [...this.byOrigin.values()]) this.close(l);
5782
+ if (this.sweepTimer) clearInterval(this.sweepTimer);
5783
+ this.sweepTimer = null;
5784
+ }
5785
+ /** Close listeners idle for idleMs (no request and no open WebSocket); exposed for tests with a fake clock */
5786
+ sweep() {
5787
+ const now = this.now();
5788
+ for (const l of [...this.byOrigin.values()]) {
5789
+ if (l.tunnels === 0 && now - l.lastUsed > this.idleMs) {
5790
+ this.opts.log?.("info", `preview proxy for ${l.target.origin} idle, closing`);
5791
+ this.close(l);
5792
+ }
5793
+ }
5794
+ if (this.byOrigin.size === 0 && this.sweepTimer) {
5795
+ clearInterval(this.sweepTimer);
5796
+ this.sweepTimer = null;
5797
+ }
5798
+ }
5799
+ ensureSweep() {
5800
+ if (this.sweepTimer) return;
5801
+ this.sweepTimer = setInterval(() => this.sweep(), 60 * 1e3);
5802
+ this.sweepTimer.unref();
5803
+ }
5804
+ close(l) {
5805
+ if (this.byOrigin.get(l.target.origin) === l) this.byOrigin.delete(l.target.origin);
5806
+ try {
5807
+ l.server.close();
5808
+ } catch {
5809
+ }
5810
+ for (const s of l.sockets) s.destroy();
5811
+ l.sockets.clear();
5812
+ }
5813
+ async listen(t) {
5814
+ while (this.byOrigin.size >= this.maxTargets) {
5815
+ const oldest = [...this.byOrigin.values()].sort((a2, b) => a2.lastUsed - b.lastUsed)[0];
5816
+ if (!oldest) break;
5817
+ this.close(oldest);
5818
+ }
5819
+ const server = createServer2();
5820
+ const l = {
5821
+ target: t,
5822
+ server,
5823
+ port: 0,
5824
+ authority: "",
5825
+ proxyOrigin: "",
5826
+ nonce: "",
5827
+ canaryAuthority: "",
5828
+ canaryNonce: "",
5829
+ magpie: false,
5830
+ checkedAt: 0,
5831
+ resolving: null,
5832
+ lastUsed: this.now(),
5833
+ sockets: /* @__PURE__ */ new Set(),
5834
+ tunnels: 0,
5835
+ addr: null
5836
+ };
5837
+ server.on("connection", (s) => {
5838
+ l.sockets.add(s);
5839
+ s.on("close", () => l.sockets.delete(s));
5840
+ });
5841
+ server.on("request", (req, res) => this.onRequest(l, req, res));
5842
+ server.on("upgrade", (req, socket, head) => this.onUpgrade(l, req, socket, head));
5843
+ server.on("clientError", (_e, socket) => {
5844
+ try {
5845
+ socket.destroy();
5846
+ } catch {
5847
+ }
5848
+ });
5849
+ await new Promise((resolve4, reject) => {
5850
+ server.once("error", reject);
5851
+ server.listen(0, "127.0.0.1", () => resolve4());
5852
+ });
5853
+ const a = server.address();
5854
+ l.port = a && typeof a === "object" ? a.port : 0;
5855
+ l.authority = `ac${randomBytes3(16).toString("hex")}.localhost:${l.port}`;
5856
+ l.proxyOrigin = `http://${l.authority}`;
5857
+ this.byOrigin.set(t.origin, l);
5858
+ this.opts.log?.("info", `preview proxy 127.0.0.1:${l.port} \u2192 ${t.origin}`);
5859
+ return l;
5860
+ }
5861
+ // ---- request gate shared by HTTP and WS ----
5862
+ /** null = refuse; otherwise which name matched (canary is ping-only) */
5863
+ gate(l, req) {
5864
+ const host = String(req.headers.host ?? "").toLowerCase();
5865
+ let role = null;
5866
+ if (l.authority && tokenEqual(host, l.authority)) role = "key";
5867
+ else if (l.canaryAuthority && tokenEqual(host, l.canaryAuthority)) role = "canary";
5868
+ else {
5869
+ return checkHost(req.headers.host, l.port).ok ? { code: 403, why: "preview_key" } : { code: 421, why: "bad_host" };
5870
+ }
5871
+ const o = req.headers.origin;
5872
+ if (o !== void 0 && o !== "" && o !== "null") {
5873
+ if (role === "key" && o.toLowerCase() !== l.proxyOrigin) return { code: 403, why: "bad_origin" };
5874
+ if (role === "canary") return { code: 403, why: "bad_origin" };
5875
+ }
5876
+ return { role };
5877
+ }
5878
+ /** A refusal: plain text, no CORS headers (the page that sent it can't read it), nothing forwarded */
5879
+ refuse(res, req, code, why) {
5880
+ res.writeHead(code, {
5881
+ "content-type": "text/plain; charset=utf-8",
5882
+ "cache-control": "no-store",
5883
+ "x-content-type-options": "nosniff",
5884
+ "content-security-policy": "sandbox; default-src 'none'",
5885
+ "referrer-policy": "no-referrer"
5886
+ });
5887
+ res.end(req.method === "HEAD" ? void 0 : why);
5888
+ req.resume();
5889
+ }
5890
+ onRequest(l, req, res) {
5891
+ const g = this.gate(l, req);
5892
+ if ("code" in g) {
5893
+ this.refuse(res, req, g.code, g.why);
5894
+ return;
5895
+ }
5896
+ l.lastUsed = this.now();
5897
+ const path3 = (req.url ?? "/").split("?")[0];
5898
+ if (path3 === PREVIEW_PING_PATH) {
5899
+ const body = g.role === "canary" ? l.canaryNonce : l.nonce;
5900
+ res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", "access-control-allow-origin": "null", "referrer-policy": "no-referrer" });
5901
+ res.end(req.method === "HEAD" ? void 0 : body);
5902
+ req.resume();
5903
+ return;
5904
+ }
5905
+ if (g.role === "canary") {
5906
+ this.refuse(res, req, 403, "preview_canary");
5907
+ return;
5908
+ }
5909
+ if (l.magpie) {
5910
+ this.refuse(res, req, 403, "magpie");
5911
+ return;
5912
+ }
5913
+ if (path3 === PREVIEW_SCRIPT_PATH) {
5914
+ res.writeHead(200, {
5915
+ "content-type": "text/javascript; charset=utf-8",
5916
+ "cache-control": "no-store",
5917
+ "access-control-allow-origin": "null",
5918
+ "x-content-type-options": "nosniff",
5919
+ "referrer-policy": "no-referrer"
5920
+ });
5921
+ res.end(req.method === "HEAD" ? void 0 : PREVIEW_SCRIPT);
5922
+ return;
5923
+ }
5924
+ if (req.method === "OPTIONS" && req.headers["access-control-request-method"]) {
5925
+ const h = {
5926
+ "access-control-allow-origin": "null",
5927
+ "access-control-allow-methods": String(req.headers["access-control-request-method"]).slice(0, 200) + ", GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD",
5928
+ "access-control-max-age": "600",
5929
+ vary: "origin"
5930
+ };
5931
+ const rh = req.headers["access-control-request-headers"];
5932
+ if (typeof rh === "string") h["access-control-allow-headers"] = rh.slice(0, 2e3);
5933
+ res.writeHead(204, h);
5934
+ res.end();
5935
+ req.resume();
5936
+ return;
5937
+ }
5938
+ void this.forward(l, req, res);
5939
+ }
5940
+ /**
5941
+ * The address to forward to, once the target is known not to be Magpie's gateway (asked again after IDENTITY_TTL).
5942
+ * Fails closed: no answer to the identity check → null (error page), Magpie → l.magpie (refused from then on).
5943
+ */
5944
+ resolveAddr(l) {
5945
+ if (l.magpie) return Promise.resolve(null);
5946
+ if (l.addr && this.now() - l.checkedAt < IDENTITY_TTL) return Promise.resolve(l.addr);
5947
+ if (!l.resolving) {
5948
+ l.resolving = (async () => {
5949
+ const addr = l.addr ?? await this.probeAddr(l);
5950
+ if (!addr) return null;
5951
+ const who = await identify(l.target, [addr], this.identityTimeout);
5952
+ if (who === "magpie") {
5953
+ l.magpie = true;
5954
+ l.addr = null;
5955
+ this.opts.log?.("warn", `preview proxy for ${l.target.origin}: the target answers like Magpie's gateway, refusing`);
5956
+ return null;
5957
+ }
5958
+ if (who === "none") {
5959
+ l.addr = null;
5960
+ return null;
5961
+ }
5962
+ l.addr = addr;
5963
+ l.checkedAt = this.now();
5964
+ return addr;
5965
+ })().finally(() => {
5966
+ l.resolving = null;
5967
+ });
5968
+ }
5969
+ return l.resolving;
5970
+ }
5971
+ async probeAddr(l) {
5972
+ for (const host of l.target.connect) {
5973
+ const ok = await new Promise((resolve4) => {
5974
+ const s = netConnect({ host, port: l.target.port });
5975
+ const done = (v) => {
5976
+ s.removeAllListeners();
5977
+ s.destroy();
5978
+ resolve4(v);
5979
+ };
5980
+ s.setTimeout(3e3, () => done(false));
5981
+ s.once("connect", () => done(true));
5982
+ s.once("error", () => done(false));
5983
+ });
5984
+ if (ok) return host;
5985
+ }
5986
+ return null;
5987
+ }
5988
+ servername(l) {
5989
+ return isIP(l.target.hostname) ? void 0 : l.target.hostname;
5990
+ }
5991
+ async forward(l, req, res) {
5992
+ const addr = await this.resolveAddr(l);
5993
+ if (l.magpie) {
5994
+ this.refuse(res, req, 403, "magpie");
5995
+ return;
5996
+ }
5997
+ if (!addr) {
5998
+ this.errorPage(l, req, res);
5999
+ return;
6000
+ }
6001
+ const headers = rewriteRequestHeaders(req.headers, l.target, l.proxyOrigin, false);
6002
+ const reqFn = l.target.secure ? httpsRequest : httpRequest;
6003
+ const up = reqFn({
6004
+ host: addr,
6005
+ port: l.target.port,
6006
+ method: req.method,
6007
+ path: req.url ?? "/",
6008
+ headers,
6009
+ ...l.target.secure ? { rejectUnauthorized: false, servername: this.servername(l) } : {}
6010
+ });
6011
+ up.on("response", (ur) => this.respond(l, req, res, ur));
6012
+ up.on("error", (e) => {
6013
+ if (RETRY_CODES.has(e.code ?? "")) l.addr = null;
6014
+ if (!res.headersSent) this.errorPage(l, req, res);
6015
+ else res.destroy();
6016
+ });
6017
+ req.on("aborted", () => up.destroy());
6018
+ res.on("close", () => {
6019
+ if (!res.writableFinished) up.destroy();
6020
+ });
6021
+ req.pipe(up);
6022
+ }
6023
+ respond(l, req, res, ur) {
6024
+ l.lastUsed = this.now();
6025
+ if (looksLikeMagpie(ur.headers)) {
6026
+ l.magpie = true;
6027
+ l.addr = null;
6028
+ ur.resume();
6029
+ this.refuse(res, req, 403, "magpie");
6030
+ return;
6031
+ }
6032
+ const h = rewriteResponseHeaders(ur.headers, l.target, l.proxyOrigin);
6033
+ const status = ur.statusCode ?? 502;
6034
+ const ctype = String(ur.headers["content-type"] ?? "").toLowerCase();
6035
+ const encoded = !!ur.headers["content-encoding"] && ur.headers["content-encoding"] !== "identity";
6036
+ const html = ctype.includes("text/html") && !encoded && req.method !== "HEAD" && status !== 204 && status !== 304;
6037
+ if (!html) {
6038
+ res.writeHead(status, h);
6039
+ ur.pipe(res);
6040
+ return;
6041
+ }
6042
+ const chunks = [];
6043
+ let size = 0;
6044
+ let passthrough = false;
6045
+ ur.on("data", (c) => {
6046
+ if (passthrough) return;
6047
+ size += c.length;
6048
+ chunks.push(c);
6049
+ if (size > this.maxHtml) {
6050
+ passthrough = true;
6051
+ res.writeHead(status, h);
6052
+ for (const x of chunks) res.write(x);
6053
+ ur.removeAllListeners("data");
6054
+ ur.removeAllListeners("end");
6055
+ ur.pipe(res);
6056
+ }
6057
+ });
6058
+ ur.on("end", () => {
6059
+ if (passthrough) return;
6060
+ const out = Buffer.from(injectScriptTag(Buffer.concat(chunks).toString("latin1")), "latin1");
6061
+ h["content-length"] = String(out.length);
6062
+ res.writeHead(status, h);
6063
+ res.end(out);
6064
+ });
6065
+ ur.on("error", () => res.destroy());
6066
+ }
6067
+ errorPage(l, req, res) {
6068
+ const where = displayOrigin(l.target.origin).replace(/^https?:\/\//, "");
6069
+ const body = `<!doctype html><html><head><meta charset="utf-8"><title>${esc(where)}</title>
6070
+ <style>body{font:13px/1.6 system-ui,sans-serif;color:#525252;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#fff}
6071
+ @media (prefers-color-scheme:dark){body{background:#1a1a1a;color:#a3a3a3}button{background:#262626;color:#e5e5e5;border-color:#3a3a3a}}
6072
+ .b{text-align:center;max-width:420px;padding:20px}code{font-family:ui-monospace,monospace}button{margin-top:12px;padding:5px 14px;border:1px solid #d4d4d4;border-radius:6px;background:#fafafa;cursor:pointer;font:inherit}</style></head>
6073
+ <body><div class="b"><div><code>${esc(where)}</code> \u6CA1\u6709\u54CD\u5E94\uFF0Cdev server \u542F\u52A8\u4E86\u5417\uFF1F</div><div><code>${esc(where)}</code> is not responding. Is the dev server running?</div>
6074
+ <button onclick="location.reload()">\u91CD\u8BD5 / Retry</button></div></body></html>`;
6075
+ res.writeHead(502, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "access-control-allow-origin": "null" });
6076
+ res.end(req.method === "HEAD" ? void 0 : body);
6077
+ req.resume();
6078
+ }
6079
+ // ---- WebSocket (HMR) ----
6080
+ onUpgrade(l, req, socket, head) {
6081
+ const g = this.gate(l, req);
6082
+ if ("code" in g || g.role === "canary" || l.magpie) {
6083
+ const code = "code" in g ? g.code : 403;
6084
+ socket.end(`HTTP/1.1 ${code} ${code === 421 ? "Misdirected Request" : "Forbidden"}\r
6085
+ connection: close\r
6086
+ \r
6087
+ `);
6088
+ return;
6089
+ }
6090
+ l.lastUsed = this.now();
6091
+ socket.on("error", () => {
6092
+ });
6093
+ void this.resolveAddr(l).then((addr) => {
6094
+ if (!addr || socket.destroyed) {
6095
+ socket.end("HTTP/1.1 502 Bad Gateway\r\nconnection: close\r\n\r\n");
6096
+ return;
6097
+ }
6098
+ const up = l.target.secure ? tlsConnect({ host: addr, port: l.target.port, rejectUnauthorized: false, servername: this.servername(l) }) : netConnect({ host: addr, port: l.target.port });
6099
+ const ready = l.target.secure ? "secureConnect" : "connect";
6100
+ l.tunnels++;
6101
+ let counted = true;
6102
+ const done = () => {
6103
+ if (counted) {
6104
+ counted = false;
6105
+ l.tunnels--;
6106
+ l.lastUsed = this.now();
6107
+ }
6108
+ up.destroy();
6109
+ socket.destroy();
6110
+ };
6111
+ up.once(ready, () => {
6112
+ const lines = [`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/1.1`];
6113
+ const hs = rewriteRequestHeaders(req.headers, l.target, l.proxyOrigin, true);
6114
+ for (const [k, v] of Object.entries(hs)) {
6115
+ if (v === void 0) continue;
6116
+ for (const one of Array.isArray(v) ? v : [v]) lines.push(`${k}: ${String(one).replace(/[\r\n]/g, "")}`);
6117
+ }
6118
+ up.write(lines.join("\r\n") + "\r\n\r\n");
6119
+ if (head?.length) up.write(head);
6120
+ up.pipe(socket);
6121
+ socket.pipe(up);
6122
+ });
6123
+ up.on("data", () => {
6124
+ l.lastUsed = this.now();
6125
+ });
6126
+ up.on("error", done);
6127
+ up.on("close", done);
6128
+ socket.on("close", done);
6129
+ });
6130
+ }
6131
+ };
6132
+ }
6133
+ });
6134
+
6135
+ // daemon/src/preview/host.ts
6136
+ import os from "node:os";
6137
+ function previewHost(platform = os.platform(), release2 = os.release()) {
6138
+ if (platform !== "darwin") return { platform, darwinMajor: null };
6139
+ const m = /^(\d+)\./.exec(String(release2 ?? "").trim()) ?? /^(\d+)$/.exec(String(release2 ?? "").trim());
6140
+ const major = m ? Number(m[1]) : NaN;
6141
+ return { platform, darwinMajor: Number.isInteger(major) && major > 0 ? major : null };
6142
+ }
6143
+ var init_host = __esm({
6144
+ "daemon/src/preview/host.ts"() {
6145
+ }
6146
+ });
6147
+
5225
6148
  // daemon/src/ws/methods.ts
5226
6149
  import { z } from "zod";
5227
- import { existsSync as existsSync8, statSync as statSync4 } from "node:fs";
6150
+ import { existsSync as existsSync9, statSync as statSync4 } from "node:fs";
5228
6151
  import { mkdir as mkdir3, writeFile as writeFile4 } from "node:fs/promises";
5229
6152
  import { homedir as homedir8 } from "node:os";
5230
6153
  import { dirname as dirname3, join as join12, win32 as win32Path } from "node:path";
5231
- import { execFile as execFile3 } from "node:child_process";
6154
+ import { execFile as execFile4 } from "node:child_process";
5232
6155
  function wrap(schema, fn) {
5233
6156
  return (params, ctx) => {
5234
6157
  const r = schema.safeParse(params ?? {});
@@ -5240,6 +6163,10 @@ function wrap(schema, fn) {
5240
6163
  }
5241
6164
  function registerMethods(gw, svc) {
5242
6165
  const zEmpty = z.object({}).passthrough().optional();
6166
+ gw.method("app.setLocale", wrap(z.object({ lang: z.string().max(32) }), (p) => {
6167
+ setAppLocale(p.lang);
6168
+ return true;
6169
+ }));
5243
6170
  gw.method("app.info", wrap(zEmpty, () => ({
5244
6171
  version: svc.cfg.version,
5245
6172
  dataDir: svc.cfg.dataDir,
@@ -5250,6 +6177,33 @@ function registerMethods(gw, svc) {
5250
6177
  setTimeout(() => process.exit(0), 100);
5251
6178
  return { ok: true };
5252
6179
  }));
6180
+ gw.method("app.openExternal", wrap(z.object({ url: z.string().min(1).max(MAX_PREVIEW_URL) }), async (p) => openExternal(p.url)));
6181
+ gw.method("preview.open", wrap(
6182
+ z.object({ url: z.string().min(1).max(MAX_PREVIEW_URL), threadId: z.string().max(200).optional() }),
6183
+ async (p) => {
6184
+ if (!svc.preview) throw new ApiError("unavailable", tr("err.previewUnavailable"));
6185
+ let r;
6186
+ try {
6187
+ r = await svc.preview.open(p.url);
6188
+ } catch (e) {
6189
+ if (e instanceof PreviewError) {
6190
+ throw new ApiError(e.code === "denied_port" ? "preview_denied" : "preview_url", tr(e.code === "denied_port" ? "err.previewDenied" : "err.previewUrl"));
6191
+ }
6192
+ throw e;
6193
+ }
6194
+ if (p.threadId) {
6195
+ const meta = await svc.threads.get(p.threadId);
6196
+ if (meta && meta.previewUrl !== r.url) {
6197
+ const next = await svc.threads.patch(p.threadId, { previewUrl: r.url });
6198
+ gw.publish("sidebar", { type: "thread.upsert", thread: next });
6199
+ }
6200
+ }
6201
+ return { ...r, host: previewHost() };
6202
+ }
6203
+ ));
6204
+ gw.method("preview.release", wrap(z.object({ url: z.string().min(1).max(MAX_PREVIEW_URL) }), (p) => ({
6205
+ ok: svc.preview?.release(p.url) ?? false
6206
+ })));
5253
6207
  gw.method("settings.get", wrap(zEmpty, () => svc.settings.get()));
5254
6208
  gw.method("settings.set", wrap(z.object({ patch: z.record(z.string(), z.unknown()) }), async (p) => {
5255
6209
  const checked = parseSettingsPatch(p.patch);
@@ -5352,10 +6306,10 @@ function registerMethods(gw, svc) {
5352
6306
  gw.method("projects.open", wrap(z.object({ id: z.string() }), async (p) => {
5353
6307
  const proj = await svc.projects.get(p.id);
5354
6308
  if (!proj) throw new ApiError("not_found", tr("err.projectMissing"));
5355
- if (!existsSync8(proj.path)) throw new ApiError("bad_path", tr("err.dirMissing"));
6309
+ if (!existsSync9(proj.path)) throw new ApiError("bad_path", tr("err.dirMissing"));
5356
6310
  const cmd = process.platform === "win32" ? "explorer.exe" : process.platform === "darwin" ? "open" : "xdg-open";
5357
6311
  const target = process.platform === "win32" ? win32Path.normalize(proj.path) : proj.path;
5358
- execFile3(cmd, [target], { windowsHide: true }, () => {
6312
+ execFile4(cmd, [target], { windowsHide: true }, () => {
5359
6313
  }).on("error", () => {
5360
6314
  });
5361
6315
  return { ok: true };
@@ -5364,7 +6318,7 @@ function registerMethods(gw, svc) {
5364
6318
  let exists = false;
5365
6319
  let isDir2 = false;
5366
6320
  try {
5367
- exists = existsSync8(p.path);
6321
+ exists = existsSync9(p.path);
5368
6322
  isDir2 = exists && statSync4(p.path).isDirectory();
5369
6323
  } catch {
5370
6324
  }
@@ -5393,7 +6347,7 @@ function registerMethods(gw, svc) {
5393
6347
  async (p) => {
5394
6348
  const proj = await svc.projects.get(p.projectId);
5395
6349
  if (!proj) throw new ApiError("not_found", tr("err.projectMissing"));
5396
- if (!existsSync8(proj.path)) throw new ApiError("bad_path", tr("err.projectDirGone"));
6350
+ if (!existsSync9(proj.path)) throw new ApiError("bad_path", tr("err.projectDirGone"));
5397
6351
  if (!p.providerSessionId && !isProviderEnabled(await svc.settings.get(), p.provider)) {
5398
6352
  throw new ApiError("bad_params", tr("provider.disabledNamed", { name: catalogById(p.provider)?.name ?? p.provider }));
5399
6353
  }
@@ -5429,7 +6383,8 @@ function registerMethods(gw, svc) {
5429
6383
  id: z.string(),
5430
6384
  title: z.string().optional(),
5431
6385
  pinned: z.boolean().optional(),
5432
- archived: z.boolean().optional()
6386
+ archived: z.boolean().optional(),
6387
+ previewUrl: z.string().max(MAX_PREVIEW_URL).nullable().optional()
5433
6388
  }),
5434
6389
  async (p) => {
5435
6390
  const meta = await svc.threads.get(p.id);
@@ -5441,6 +6396,14 @@ function registerMethods(gw, svc) {
5441
6396
  }
5442
6397
  if (p.pinned !== void 0) patch.pinned = p.pinned;
5443
6398
  if (p.archived !== void 0) patch.archived = p.archived;
6399
+ if (p.previewUrl !== void 0) {
6400
+ if (p.previewUrl === null) patch.previewUrl = null;
6401
+ else {
6402
+ const c = checkPreviewUrl(p.previewUrl);
6403
+ if (!c.ok) throw new ApiError("bad_params", tr("err.previewUrl"));
6404
+ patch.previewUrl = c.target.href;
6405
+ }
6406
+ }
5444
6407
  const next = await svc.threads.patch(p.id, patch);
5445
6408
  gw.publish("sidebar", { type: "thread.upsert", thread: next });
5446
6409
  return next;
@@ -5457,7 +6420,7 @@ function registerMethods(gw, svc) {
5457
6420
  const projectPath = async (projectId) => {
5458
6421
  const proj = await svc.projects.get(projectId);
5459
6422
  if (!proj) throw new ApiError("not_found", tr("err.projectMissing"));
5460
- if (!existsSync8(proj.path)) throw new ApiError("bad_path", tr("err.projectDirGone"));
6423
+ if (!existsSync9(proj.path)) throw new ApiError("bad_path", tr("err.projectDirGone"));
5461
6424
  return proj.path;
5462
6425
  };
5463
6426
  gw.method("git.status", wrap(
@@ -5564,10 +6527,12 @@ function registerMethods(gw, svc) {
5564
6527
  id: z.string(),
5565
6528
  text: z.string(),
5566
6529
  attachments: z.array(z.object({ path: z.string(), mime: z.string(), name: z.string().optional() })).optional(),
6530
+ // 1.2: paths dragged from the Files panel, relative to the project (the daemon resolves and checks them)
6531
+ mentions: z.array(z.object({ rel: z.string().min(1).max(4096), isDir: z.boolean().optional() })).max(MAX_MENTIONS).optional(),
5567
6532
  behavior: z.enum(["queue", "steer"]).optional()
5568
6533
  }),
5569
6534
  async (p) => {
5570
- const delivered = await needManager().send(p.id, { text: p.text, attachments: p.attachments }, p.behavior ?? "queue");
6535
+ const delivered = await needManager().send(p.id, { text: p.text, attachments: p.attachments, mentions: p.mentions }, p.behavior ?? "queue");
5571
6536
  return { ok: true, delivered: delivered ?? "started" };
5572
6537
  }
5573
6538
  ));
@@ -5638,11 +6603,13 @@ function registerMethods(gw, svc) {
5638
6603
  L.push(`> ${meta.provider} \xB7 ${meta.model ?? tr("export.defaultModel")} \xB7 ${meta.mode} \xB7 ${new Date(meta.createdAt).toLocaleString(intlLocale())}`, "");
5639
6604
  for (const e of events) {
5640
6605
  switch (e.type) {
5641
- case "user_message":
6606
+ case "user_message": {
6607
+ const refs = e.mentions?.length ? "\n\n" + e.mentions.map((m) => `- \`${m.path}${m.isDir ? "/" : ""}\``).join("\n") : "";
5642
6608
  L.push(`**${tr("export.me")}**${e.steered ? tr("export.steered") : ""}:
5643
6609
 
5644
- ${e.text}`, "");
6610
+ ${e.text}${refs}`, "");
5645
6611
  break;
6612
+ }
5646
6613
  case "assistant_message":
5647
6614
  L.push(e.text, "");
5648
6615
  break;
@@ -5691,7 +6658,7 @@ ${e.text}
5691
6658
  await mkdir3(dir, { recursive: true });
5692
6659
  }
5693
6660
  let dest = join12(dir, filename);
5694
- if (existsSync8(dest)) {
6661
+ if (existsSync9(dest)) {
5695
6662
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
5696
6663
  dest = join12(dir, `${safe}-${stamp2}.md`);
5697
6664
  }
@@ -5702,18 +6669,18 @@ ${e.text}
5702
6669
  }
5703
6670
  function revealInFileManager(filePath) {
5704
6671
  if (process.platform === "win32") {
5705
- execFile3("explorer.exe", ["/select," + win32Path.normalize(filePath)], { windowsHide: true }, () => {
6672
+ execFile4("explorer.exe", ["/select," + win32Path.normalize(filePath)], { windowsHide: true }, () => {
5706
6673
  }).on("error", () => {
5707
6674
  });
5708
6675
  return;
5709
6676
  }
5710
6677
  if (process.platform === "darwin") {
5711
- execFile3("open", ["-R", filePath], () => {
6678
+ execFile4("open", ["-R", filePath], () => {
5712
6679
  }).on("error", () => {
5713
6680
  });
5714
6681
  return;
5715
6682
  }
5716
- execFile3("xdg-open", [dirname3(filePath)], () => {
6683
+ execFile4("xdg-open", [dirname3(filePath)], () => {
5717
6684
  }).on("error", () => {
5718
6685
  });
5719
6686
  }
@@ -5730,6 +6697,11 @@ var init_methods = __esm({
5730
6697
  init_launch();
5731
6698
  init_modes();
5732
6699
  init_i18n();
6700
+ init_mentions();
6701
+ init_openExternal();
6702
+ init_proxy();
6703
+ init_url();
6704
+ init_host();
5733
6705
  threadModeSchema = z.enum(["plan", "default", "acceptEdits", "bypassPermissions", "ask"]);
5734
6706
  ApiError = class extends Error {
5735
6707
  code;
@@ -5790,10 +6762,14 @@ function sanitizeStored(raw) {
5790
6762
  }
5791
6763
  return out;
5792
6764
  }
5793
- var DEFAULT_SETTINGS, IDLE_CLOSE_MIN, IDLE_CLOSE_MAX, mode, id, settingsPatchSchema, SettingsStore;
6765
+ var CONTENT_WIDTHS, CHAT_FONT_MIN, CHAT_FONT_MAX, CHAT_FONT_DEFAULT, DEFAULT_SETTINGS, IDLE_CLOSE_MIN, IDLE_CLOSE_MAX, mode, id, settingsPatchSchema, SettingsStore;
5794
6766
  var init_settings = __esm({
5795
6767
  "daemon/src/store/settings.ts"() {
5796
6768
  init_atomic();
6769
+ CONTENT_WIDTHS = ["narrow", "default", "wide", "full"];
6770
+ CHAT_FONT_MIN = 12;
6771
+ CHAT_FONT_MAX = 18;
6772
+ CHAT_FONT_DEFAULT = 13;
5797
6773
  DEFAULT_SETTINGS = {
5798
6774
  defaultProvider: "claude",
5799
6775
  defaultMode: "default",
@@ -5801,6 +6777,8 @@ var init_settings = __esm({
5801
6777
  runningSend: "queue",
5802
6778
  showThinking: true,
5803
6779
  theme: "auto",
6780
+ contentWidth: "default",
6781
+ chatFontSize: CHAT_FONT_DEFAULT,
5804
6782
  idleCloseMinutes: 30,
5805
6783
  collapsedProjects: [],
5806
6784
  panel: { open: false, tab: "changes", width: 440 },
@@ -5821,10 +6799,13 @@ var init_settings = __esm({
5821
6799
  showThinking: z2.boolean(),
5822
6800
  // auto = follow the host: Magpie's light/dark inside Magpie, the OS outside it
5823
6801
  theme: z2.enum(["auto", "light", "dark"]),
6802
+ contentWidth: z2.enum(CONTENT_WIDTHS),
6803
+ chatFontSize: z2.number().int().min(CHAT_FONT_MIN).max(CHAT_FONT_MAX),
5824
6804
  idleCloseMinutes: z2.number().int().min(IDLE_CLOSE_MIN).max(IDLE_CLOSE_MAX),
5825
6805
  collapsedProjects: z2.array(id).max(1e3),
5826
6806
  panel: z2.object({ open: z2.boolean(), tab: z2.string().max(40), width: z2.number().min(0).max(5e3) }).partial(),
5827
6807
  sidebarWidth: z2.number().min(0).max(5e3),
6808
+ sidebarHidden: z2.boolean(),
5828
6809
  lastProjectId: id.nullable(),
5829
6810
  lastThreadId: id.nullable(),
5830
6811
  recentDirs: z2.array(z2.string().max(4096)).max(50),
@@ -5919,8 +6900,8 @@ __export(legacy_exports, {
5919
6900
  migrateLegacy: () => migrateLegacy
5920
6901
  });
5921
6902
  import { cp as cp2, mkdir as mkdir8, readdir as readdir6, readFile as readFile18 } from "node:fs/promises";
5922
- import { existsSync as existsSync12 } from "node:fs";
5923
- import { join as join19 } from "node:path";
6903
+ import { existsSync as existsSync13 } from "node:fs";
6904
+ import { join as join20 } from "node:path";
5924
6905
  function stamp() {
5925
6906
  const d = /* @__PURE__ */ new Date();
5926
6907
  const p = (n) => String(n).padStart(2, "0");
@@ -5929,19 +6910,19 @@ function stamp() {
5929
6910
  async function migrateLegacy(dataDir, stores) {
5930
6911
  const settings = await stores.settings.get();
5931
6912
  if (settings.v1Migrated) return { migrated: 0, skipped: true };
5932
- const oldDir = join19(dataDir, "sessions");
5933
- const oldSettingsPath = join19(dataDir, "settings.json");
5934
- if (!existsSync12(oldDir) && !existsSync12(oldSettingsPath)) {
6913
+ const oldDir = join20(dataDir, "sessions");
6914
+ const oldSettingsPath = join20(dataDir, "settings.json");
6915
+ if (!existsSync13(oldDir) && !existsSync13(oldSettingsPath)) {
5935
6916
  await stores.settings.set({ v1Migrated: true });
5936
6917
  return { migrated: 0, skipped: true };
5937
6918
  }
5938
- const backup = join19(dataDir, "trash", `legacy-${stamp()}`);
6919
+ const backup = join20(dataDir, "trash", `legacy-${stamp()}`);
5939
6920
  await mkdir8(backup, { recursive: true });
5940
- if (existsSync12(oldDir)) await cp2(oldDir, join19(backup, "sessions"), { recursive: true }).catch(() => {
6921
+ if (existsSync13(oldDir)) await cp2(oldDir, join20(backup, "sessions"), { recursive: true }).catch(() => {
5941
6922
  });
5942
- if (existsSync12(oldSettingsPath)) await cp2(oldSettingsPath, join19(backup, "settings.json")).catch(() => {
6923
+ if (existsSync13(oldSettingsPath)) await cp2(oldSettingsPath, join20(backup, "settings.json")).catch(() => {
5943
6924
  });
5944
- const oldSettings = existsSync12(oldSettingsPath) ? JSON.parse(await readFile18(oldSettingsPath, "utf8").catch(() => "{}")) : {};
6925
+ const oldSettings = existsSync13(oldSettingsPath) ? JSON.parse(await readFile18(oldSettingsPath, "utf8").catch(() => "{}")) : {};
5945
6926
  const projectIdByPath = /* @__PURE__ */ new Map();
5946
6927
  const lc = (p) => p.replace(/\\/g, "/").toLowerCase().replace(/\/+$/, "");
5947
6928
  for (const p of oldSettings.projects ?? []) {
@@ -5955,13 +6936,13 @@ async function migrateLegacy(dataDir, stores) {
5955
6936
  }
5956
6937
  let sessionIds = [];
5957
6938
  try {
5958
- sessionIds = (await readdir6(oldDir)).filter((d) => validId(d) && existsSync12(join19(oldDir, d, "meta.json")));
6939
+ sessionIds = (await readdir6(oldDir)).filter((d) => validId(d) && existsSync13(join20(oldDir, d, "meta.json")));
5959
6940
  } catch {
5960
6941
  }
5961
6942
  const metas = [];
5962
6943
  for (const id2 of sessionIds) {
5963
6944
  try {
5964
- const m = JSON.parse(await readFile18(join19(oldDir, id2, "meta.json"), "utf8"));
6945
+ const m = JSON.parse(await readFile18(join20(oldDir, id2, "meta.json"), "utf8"));
5965
6946
  if (m?.cwd) metas.push(m);
5966
6947
  } catch {
5967
6948
  }
@@ -5975,7 +6956,7 @@ async function migrateLegacy(dataDir, stores) {
5975
6956
  extraDirs.push(m.cwd);
5976
6957
  }
5977
6958
  for (const d of extraDirs) {
5978
- if (!existsSync12(d)) continue;
6959
+ if (!existsSync13(d)) continue;
5979
6960
  try {
5980
6961
  const name = d.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? d;
5981
6962
  const proj = await stores.projects.create(d, name);
@@ -5994,7 +6975,7 @@ async function migrateLegacy(dataDir, stores) {
5994
6975
  return best ? projectIdByPath.get(best) : null;
5995
6976
  };
5996
6977
  let migrated = 0;
5997
- const threadsDir = join19(dataDir, "threads");
6978
+ const threadsDir = join20(dataDir, "threads");
5998
6979
  for (const m of metas) {
5999
6980
  try {
6000
6981
  const projectId = projectOf(m.cwd);
@@ -6031,10 +7012,10 @@ async function migrateLegacy(dataDir, stores) {
6031
7012
  importedFrom: m.cliSessionId ? { provider, sessionId: m.cliSessionId } : void 0,
6032
7013
  seq: 0
6033
7014
  };
6034
- const tdir = join19(threadsDir, meta.id);
6035
- if (existsSync12(tdir)) meta.id = `t_${m.id}_v1`.slice(0, 80);
6036
- await mkdir8(join19(threadsDir, meta.id), { recursive: true });
6037
- const oldEvts = (await readFile18(join19(oldDir, m.id, "events.jsonl"), "utf8").catch(() => "")).split("\n").filter((l) => l.trim()).map((l) => {
7015
+ const tdir = join20(threadsDir, meta.id);
7016
+ if (existsSync13(tdir)) meta.id = `t_${m.id}_v1`.slice(0, 80);
7017
+ await mkdir8(join20(threadsDir, meta.id), { recursive: true });
7018
+ const oldEvts = (await readFile18(join20(oldDir, m.id, "events.jsonl"), "utf8").catch(() => "")).split("\n").filter((l) => l.trim()).map((l) => {
6038
7019
  try {
6039
7020
  return JSON.parse(l);
6040
7021
  } catch {
@@ -6113,9 +7094,9 @@ async function migrateLegacy(dataDir, stores) {
6113
7094
  seq++;
6114
7095
  lines.push({ type: "tool_call", itemId: key, kind: "other", title: o.title, status: "failed", seq, ts: Date.now(), threadId: meta.id });
6115
7096
  }
6116
- for (const ev of lines) await appendJsonl(join19(threadsDir, meta.id, "timeline.jsonl"), ev);
7097
+ for (const ev of lines) await appendJsonl(join20(threadsDir, meta.id, "timeline.jsonl"), ev);
6117
7098
  meta.seq = seq;
6118
- await writeJsonAtomic(join19(threadsDir, meta.id, "meta.json"), meta);
7099
+ await writeJsonAtomic(join20(threadsDir, meta.id, "meta.json"), meta);
6119
7100
  migrated++;
6120
7101
  } catch (e) {
6121
7102
  log("warn", "\u8FC1\u79FB\uFF1A\u4F1A\u8BDD\u5931\u8D25", { id: m.id, err: String(e) });
@@ -6180,8 +7161,8 @@ var init_warm = __esm({
6180
7161
 
6181
7162
  // daemon/src/main.ts
6182
7163
  import { mkdirSync as mkdirSync2, rmSync as rmSync2, chmodSync as chmodSync2 } from "node:fs";
6183
- import { join as join20, dirname as dirname7 } from "node:path";
6184
- import { fileURLToPath as fileURLToPath3 } from "node:url";
7164
+ import { join as join21, dirname as dirname8 } from "node:path";
7165
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
6185
7166
 
6186
7167
  // daemon/src/config.ts
6187
7168
  import { randomBytes } from "node:crypto";
@@ -6236,6 +7217,7 @@ function parseConfig(argv) {
6236
7217
  let data = null;
6237
7218
  let token = null;
6238
7219
  let parentPid;
7220
+ let gatePort;
6239
7221
  for (let i = 0; i < argv.length; i++) {
6240
7222
  const a = argv[i];
6241
7223
  if (a === "--port" && argv[i + 1]) port = Number(argv[++i]);
@@ -6246,6 +7228,8 @@ function parseConfig(argv) {
6246
7228
  else if (a.startsWith("--token=")) token = a.slice(8);
6247
7229
  else if (a === "--parent-pid" && argv[i + 1]) parentPid = Number(argv[++i]);
6248
7230
  else if (a.startsWith("--parent-pid=")) parentPid = Number(a.slice(13));
7231
+ else if (a === "--gate-port" && argv[i + 1]) gatePort = Number(argv[++i]);
7232
+ else if (a.startsWith("--gate-port=")) gatePort = Number(a.slice(12));
6249
7233
  }
6250
7234
  if (port == null || !Number.isInteger(port) || port < 0 || port > 65535) {
6251
7235
  const envPort = Number(process.env.AGENT_CONSOLE_PORT || "");
@@ -6261,7 +7245,8 @@ function parseConfig(argv) {
6261
7245
  version: pkgVersion(),
6262
7246
  idleExitMinutes: Number.isFinite(idleEnv) && idleEnv > 0 ? idleEnv : 60,
6263
7247
  buildStamp: daemonBuildStamp(),
6264
- parentPid: Number.isInteger(parentPid) ? parentPid : void 0
7248
+ parentPid: Number.isInteger(parentPid) ? parentPid : void 0,
7249
+ gatePort: Number.isInteger(gatePort) && gatePort > 0 && gatePort <= 65535 ? gatePort : void 0
6265
7250
  };
6266
7251
  }
6267
7252
 
@@ -6269,35 +7254,13 @@ function parseConfig(argv) {
6269
7254
  init_log();
6270
7255
 
6271
7256
  // daemon/src/http.ts
7257
+ init_security();
7258
+ init_log();
7259
+ init_i18n();
6272
7260
  import { createServer } from "node:http";
6273
7261
  import { readFile, stat } from "node:fs/promises";
6274
7262
  import { existsSync as existsSync2 } from "node:fs";
6275
7263
  import { join as join4, normalize, extname } from "node:path";
6276
-
6277
- // daemon/src/security.ts
6278
- import { timingSafeEqual, createHash } from "node:crypto";
6279
- function checkHost(host, port) {
6280
- if (!host) return { ok: false, code: 421, message: "Host \u4E0D\u5BF9" };
6281
- const h = host.toLowerCase();
6282
- if (h === `127.0.0.1:${port}` || h === `localhost:${port}`) return { ok: true };
6283
- return { ok: false, code: 421, message: "Host \u4E0D\u5BF9" };
6284
- }
6285
- function classifyOrigin(origin, port) {
6286
- if (origin === void 0 || origin === "") return "none";
6287
- if (origin === "null") return "opaque";
6288
- const o = origin.toLowerCase();
6289
- if (o === `http://127.0.0.1:${port}` || o === `http://localhost:${port}`) return "self";
6290
- return "foreign";
6291
- }
6292
- function tokenEqual(a, b) {
6293
- const ha = createHash("sha256").update(a).digest();
6294
- const hb = createHash("sha256").update(b).digest();
6295
- return timingSafeEqual(ha, hb);
6296
- }
6297
-
6298
- // daemon/src/http.ts
6299
- init_log();
6300
- init_i18n();
6301
7264
  var MIME = {
6302
7265
  ".html": "text/html; charset=utf-8",
6303
7266
  ".js": "text/javascript; charset=utf-8",
@@ -6559,11 +7522,11 @@ init_atomic();
6559
7522
  init_paths();
6560
7523
  init_methods();
6561
7524
  import { readFile as readFile13 } from "node:fs/promises";
6562
- import { existsSync as existsSync9, statSync as statSync5 } from "node:fs";
7525
+ import { existsSync as existsSync10, statSync as statSync5 } from "node:fs";
6563
7526
  import { join as join14 } from "node:path";
6564
- import { randomBytes as randomBytes3 } from "node:crypto";
7527
+ import { randomBytes as randomBytes4 } from "node:crypto";
6565
7528
  function newId() {
6566
- return "p_" + Date.now().toString(36) + randomBytes3(3).toString("hex");
7529
+ return "p_" + Date.now().toString(36) + randomBytes4(3).toString("hex");
6567
7530
  }
6568
7531
  var ProjectsStore = class {
6569
7532
  file;
@@ -6594,7 +7557,7 @@ var ProjectsStore = class {
6594
7557
  }
6595
7558
  async create(path3, name) {
6596
7559
  if (!path3 || typeof path3 !== "string") throw new ApiError("bad_params", "\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
6597
- if (!existsSync9(path3) || !statSync5(path3).isDirectory()) {
7560
+ if (!existsSync10(path3) || !statSync5(path3).isDirectory()) {
6598
7561
  throw new ApiError("bad_path", "\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u6587\u4EF6\u5939");
6599
7562
  }
6600
7563
  const np = normPath(path3);
@@ -6636,9 +7599,9 @@ init_methods();
6636
7599
  init_i18n();
6637
7600
  import { mkdir as mkdir6, rename as rename3, readdir as readdir4, readFile as readFile15 } from "node:fs/promises";
6638
7601
  import { join as join15 } from "node:path";
6639
- import { randomBytes as randomBytes4 } from "node:crypto";
7602
+ import { randomBytes as randomBytes5 } from "node:crypto";
6640
7603
  function newId2() {
6641
- return "t_" + Date.now().toString(36) + randomBytes4(3).toString("hex");
7604
+ return "t_" + Date.now().toString(36) + randomBytes5(3).toString("hex");
6642
7605
  }
6643
7606
  var ThreadsStore = class {
6644
7607
  dir;
@@ -6784,10 +7747,11 @@ var ThreadsStore = class {
6784
7747
  };
6785
7748
 
6786
7749
  // daemon/src/ws/gateway.ts
6787
- import { WebSocketServer, WebSocket } from "ws";
6788
- import { randomBytes as randomBytes5 } from "node:crypto";
7750
+ init_security();
6789
7751
  init_log();
6790
7752
  init_i18n();
7753
+ import { WebSocketServer, WebSocket } from "ws";
7754
+ import { randomBytes as randomBytes6 } from "node:crypto";
6791
7755
  var Conn = class {
6792
7756
  id;
6793
7757
  ws;
@@ -6796,7 +7760,7 @@ var Conn = class {
6796
7760
  alive = true;
6797
7761
  lastMsg = Date.now();
6798
7762
  constructor(ws) {
6799
- this.id = "c_" + randomBytes5(4).toString("hex");
7763
+ this.id = "c_" + randomBytes6(4).toString("hex");
6800
7764
  this.ws = ws;
6801
7765
  }
6802
7766
  send(obj) {
@@ -6840,6 +7804,8 @@ var Gateway = class {
6840
7804
  methods = /* @__PURE__ */ new Map();
6841
7805
  helloTimers = /* @__PURE__ */ new Map();
6842
7806
  onTerminalInput;
7807
+ /** Called after a connection closes, with how many are left (the preview proxy closes when none are) */
7808
+ onConnectionClosed;
6843
7809
  method(name, fn) {
6844
7810
  this.methods.set(name, fn);
6845
7811
  }
@@ -6890,6 +7856,10 @@ var Gateway = class {
6890
7856
  });
6891
7857
  ws.on("close", () => {
6892
7858
  this.conns.delete(conn.id);
7859
+ try {
7860
+ this.onConnectionClosed?.(this.conns.size);
7861
+ } catch {
7862
+ }
6893
7863
  const t = this.helloTimers.get(conn.id);
6894
7864
  if (t) clearTimeout(t);
6895
7865
  this.helloTimers.delete(conn.id);
@@ -7037,6 +8007,7 @@ var Coalescer = class {
7037
8007
  init_methods();
7038
8008
  init_log();
7039
8009
  init_i18n();
8010
+ init_mentions();
7040
8011
  init_commands();
7041
8012
  var SLASH_CACHE_TTL_MS = 10 * 60 * 1e3;
7042
8013
  var MAX_WARM_IDLE = 3;
@@ -7062,6 +8033,17 @@ var AgentManager = class {
7062
8033
  resolveAttachments(refs) {
7063
8034
  return resolveAttachmentRefs(this.attachments, refs);
7064
8035
  }
8036
+ /** Mentions from the Files panel → absolute paths inside the project; a path that can't be used gets a notice */
8037
+ async resolveMentionRefs(threadId, projectId, refs) {
8038
+ if (!refs?.length) return [];
8039
+ const proj = await this.projects.get(projectId);
8040
+ if (!proj) return [];
8041
+ const { mentions, warnings } = await resolveMentions(proj.path, refs);
8042
+ if (warnings.length) {
8043
+ await this.appendRaw(threadId, { type: "notice", level: "warn", message: tr("notice.mentionSkipped", { paths: warnings.join(", ") }) });
8044
+ }
8045
+ return mentions;
8046
+ }
7065
8047
  attachmentLabels(refs) {
7066
8048
  return refs.map((a) => ({ id: a.path, name: (a.name ?? "").trim() || a.path, mime: a.mime }));
7067
8049
  }
@@ -7179,6 +8161,7 @@ var AgentManager = class {
7179
8161
  // ---- send ----
7180
8162
  async send(threadId, input, behavior = "queue") {
7181
8163
  const meta = await this.mustThread(threadId);
8164
+ const mentions = await this.resolveMentionRefs(threadId, meta.projectId, input.mentions);
7182
8165
  if (meta.archived) {
7183
8166
  await this.threads.patch(threadId, { archived: false });
7184
8167
  this.gw.publish("sidebar", { type: "thread.upsert", thread: await this.threads.get(threadId) });
@@ -7202,36 +8185,37 @@ var AgentManager = class {
7202
8185
  if (behavior === "steer" && provider?.capabilities.steer && rt.session?.steer && !rt.starting) {
7203
8186
  try {
7204
8187
  const images = await this.resolveAttachments(attachmentRefs);
7205
- await rt.session.steer({ text: input.text, images });
8188
+ await rt.session.steer({ text: withMentions(input.text, mentions), images, mentions });
7206
8189
  await this.appendRaw(threadId, {
7207
8190
  type: "user_message",
7208
8191
  text: input.text,
7209
8192
  steered: true,
7210
- attachments: this.attachmentLabels(attachmentRefs)
8193
+ attachments: this.attachmentLabels(attachmentRefs),
8194
+ ...mentions.length ? { mentions } : {}
7211
8195
  });
7212
8196
  return "steered";
7213
8197
  } catch (e) {
7214
8198
  log("info", `thread ${threadId} \u63D2\u8BDD\u5931\u8D25\uFF0C\u6539\u4E3A\u6392\u961F\uFF1A${String(e)}`);
7215
8199
  }
7216
8200
  }
7217
- const q = { id: "q" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), text: input.text, attachments: attachmentRefs };
8201
+ const q = { id: "q" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), text: input.text, attachments: attachmentRefs, ...mentions.length ? { mentions } : {} };
7218
8202
  rt.queue.push(q);
7219
8203
  this.gw.publish(`thread:${threadId}`, { type: "queue", queue: rt.queue });
7220
8204
  return "queued";
7221
8205
  }
7222
- await this.startTurn(threadId, input.text, attachmentRefs);
8206
+ await this.startTurn(threadId, input.text, attachmentRefs, mentions);
7223
8207
  return "started";
7224
8208
  }
7225
- async startTurn(threadId, text, attachmentRefs) {
8209
+ async startTurn(threadId, text, attachmentRefs, mentions = []) {
7226
8210
  const rt = this.rt(threadId);
7227
8211
  rt.starting = true;
7228
8212
  try {
7229
- await this.startTurnInner(threadId, text, attachmentRefs);
8213
+ await this.startTurnInner(threadId, text, attachmentRefs, mentions);
7230
8214
  } finally {
7231
8215
  rt.starting = false;
7232
8216
  }
7233
8217
  }
7234
- async startTurnInner(threadId, text, attachmentRefs) {
8218
+ async startTurnInner(threadId, text, attachmentRefs, mentions) {
7235
8219
  const meta = await this.mustThread(threadId);
7236
8220
  const rt = this.rt(threadId);
7237
8221
  const provider = await this.assertProviderUsable(meta.provider, { allowDisabled: !!meta.providerSessionId });
@@ -7254,12 +8238,13 @@ var AgentManager = class {
7254
8238
  await this.appendRaw(threadId, {
7255
8239
  type: "user_message",
7256
8240
  text,
7257
- attachments: this.attachmentLabels(attachmentRefs)
8241
+ attachments: this.attachmentLabels(attachmentRefs),
8242
+ ...mentions.length ? { mentions } : {}
7258
8243
  });
7259
8244
  await this.setStatus(threadId, "running");
7260
8245
  try {
7261
8246
  const images = await this.resolveAttachments(attachmentRefs);
7262
- await rt.session.send({ text, images });
8247
+ await rt.session.send({ text: withMentions(text, mentions), images, mentions });
7263
8248
  } catch (e) {
7264
8249
  const msg = String(e?.message ?? e);
7265
8250
  await this.appendRaw(threadId, { type: "notice", level: "error", message: tr("notice.sendFailed", { detail: msg }) });
@@ -7390,7 +8375,7 @@ var AgentManager = class {
7390
8375
  });
7391
8376
  if (next) {
7392
8377
  this.gw.publish(`thread:${threadId}`, { type: "queue", queue: rt.queue });
7393
- await this.startTurn(threadId, next.text, next.attachments).catch((e) => {
8378
+ await this.startTurn(threadId, next.text, next.attachments, next.mentions ?? []).catch((e) => {
7394
8379
  log("warn", `thread ${threadId} \u6392\u961F\u6D88\u606F\u542F\u52A8\u5931\u8D25\uFF1A${String(e)}`);
7395
8380
  });
7396
8381
  }
@@ -7755,15 +8740,15 @@ var AgentManager = class {
7755
8740
 
7756
8741
  // daemon/src/git/service.ts
7757
8742
  init_i18n();
7758
- import { execFile as execFile4 } from "node:child_process";
7759
- import { existsSync as existsSync10 } from "node:fs";
7760
- import { cp, mkdir as mkdir7, readFile as readFile16, rename as rename4, rm, stat as stat9 } from "node:fs/promises";
7761
- import { join as join16, normalize as normalize2, sep as sep2 } from "node:path";
8743
+ import { execFile as execFile5 } from "node:child_process";
8744
+ import { existsSync as existsSync11 } from "node:fs";
8745
+ import { cp, mkdir as mkdir7, readFile as readFile16, rename as rename4, rm, stat as stat10 } from "node:fs/promises";
8746
+ import { join as join16, normalize as normalize2, sep as sep3 } from "node:path";
7762
8747
  var TIMEOUT2 = 15e3;
7763
8748
  var DIFF_LIMIT = 1024 * 1024;
7764
8749
  function runGit(cwd, args) {
7765
8750
  return new Promise((res) => {
7766
- execFile4("git", ["-c", "core.quotepath=false", ...args], { cwd, timeout: TIMEOUT2, maxBuffer: 8 * 1024 * 1024, windowsHide: true }, (e, stdout, stderr) => {
8751
+ execFile5("git", ["-c", "core.quotepath=false", ...args], { cwd, timeout: TIMEOUT2, maxBuffer: 8 * 1024 * 1024, windowsHide: true }, (e, stdout, stderr) => {
7767
8752
  const code = e && typeof e.code === "number" ? e.code : e ? 1 : 0;
7768
8753
  const missing = e?.code === "ENOENT";
7769
8754
  res({ code, out: String(stdout ?? ""), err: missing ? tr("git.missing") : String(stderr || (e?.message ?? "")) });
@@ -7774,7 +8759,7 @@ function insidePath(projectPath, p) {
7774
8759
  const rel = normalize2(p).replace(/\\/g, "/");
7775
8760
  const abs = normalize2(join16(projectPath, rel));
7776
8761
  const proj = normalize2(projectPath);
7777
- if (abs !== proj && !abs.startsWith(proj + sep2)) return null;
8762
+ if (abs !== proj && !abs.startsWith(proj + sep3)) return null;
7778
8763
  return rel.replace(/\\/g, "/");
7779
8764
  }
7780
8765
  async function moveAcrossDevices(from, to, ops = {}) {
@@ -7822,8 +8807,8 @@ var GitService = class {
7822
8807
  const ls = await runGit(projectPath, ["ls-files", "--error-unmatch", "--", rel]);
7823
8808
  if (ls.code !== 0) {
7824
8809
  const full = join16(projectPath, rel);
7825
- if (!existsSync10(full)) return { error: tr("fs.fileMissing") };
7826
- const s = await stat9(full);
8810
+ if (!existsSync11(full)) return { error: tr("fs.fileMissing") };
8811
+ const s = await stat10(full);
7827
8812
  if (!s.isFile()) return { error: tr("fs.notFile") };
7828
8813
  if (s.size > DIFF_LIMIT) return { tooLarge: true };
7829
8814
  const text = await readFile16(full, "utf8").catch(() => null);
@@ -7856,7 +8841,7 @@ ${lines}` };
7856
8841
  return { ok: true };
7857
8842
  }
7858
8843
  const full = join16(projectPath, rel);
7859
- if (!existsSync10(full)) return { ok: false, error: tr("fs.fileMissing") };
8844
+ if (!existsSync11(full)) return { ok: false, error: tr("fs.fileMissing") };
7860
8845
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
7861
8846
  const dest = join16(dataDir, "trash", "discarded", stamp2, rel);
7862
8847
  await mkdir7(join16(dest, ".."), { recursive: true });
@@ -7871,8 +8856,8 @@ ${lines}` };
7871
8856
 
7872
8857
  // daemon/src/files/service.ts
7873
8858
  init_i18n();
7874
- import { readdir as readdir5, readFile as readFile17, stat as stat10 } from "node:fs/promises";
7875
- import { join as join17, normalize as normalize3, sep as sep3, extname as extname3 } from "node:path";
8859
+ import { readdir as readdir5, readFile as readFile17, stat as stat11 } from "node:fs/promises";
8860
+ import { join as join17, normalize as normalize3, sep as sep4, extname as extname3 } from "node:path";
7876
8861
  import ignore from "ignore";
7877
8862
  function decodeText(buf) {
7878
8863
  try {
@@ -7937,7 +8922,7 @@ function insidePath2(projectPath, p) {
7937
8922
  const rel = normalize3(p || ".").replace(/\\/g, "/");
7938
8923
  const abs = normalize3(join17(projectPath, rel));
7939
8924
  const proj = normalize3(projectPath);
7940
- if (abs !== proj && !abs.startsWith(proj + sep3)) return null;
8925
+ if (abs !== proj && !abs.startsWith(proj + sep4)) return null;
7941
8926
  return rel;
7942
8927
  }
7943
8928
  var FileService = class {
@@ -7946,7 +8931,7 @@ var FileService = class {
7946
8931
  igCache = /* @__PURE__ */ new Map();
7947
8932
  async igFor(projectPath) {
7948
8933
  const gi = join17(projectPath, ".gitignore");
7949
- const st = await stat10(gi).catch(() => null);
8934
+ const st = await stat11(gi).catch(() => null);
7950
8935
  const mtime = st?.isFile() ? st.mtimeMs : -1;
7951
8936
  const hit = this.igCache.get(projectPath);
7952
8937
  if (hit && hit.mtime === mtime) return hit.ig;
@@ -7965,7 +8950,7 @@ var FileService = class {
7965
8950
  const rel = insidePath2(projectPath, dir);
7966
8951
  if (rel === null) return { error: tr("fs.pathDenied") };
7967
8952
  const abs = join17(projectPath, rel);
7968
- const s = await stat10(abs).catch(() => null);
8953
+ const s = await stat11(abs).catch(() => null);
7969
8954
  if (!s?.isDirectory()) return { error: tr("fs.dirMissing") };
7970
8955
  const ig = await this.igFor(projectPath);
7971
8956
  const dirents = await readdir5(abs, { withFileTypes: true }).catch(() => null);
@@ -7978,7 +8963,7 @@ var FileService = class {
7978
8963
  const childRel = rel === "." ? name : `${rel}/${name}`;
7979
8964
  let isDir2 = d.isDirectory();
7980
8965
  if (d.isSymbolicLink()) {
7981
- const st = await stat10(join17(projectPath, childRel)).catch(() => null);
8966
+ const st = await stat11(join17(projectPath, childRel)).catch(() => null);
7982
8967
  if (!st) continue;
7983
8968
  isDir2 = st.isDirectory();
7984
8969
  } else if (!d.isFile() && !isDir2) {
@@ -7996,7 +8981,7 @@ var FileService = class {
7996
8981
  const rel = insidePath2(projectPath, p);
7997
8982
  if (rel === null) return { error: tr("fs.pathDenied") };
7998
8983
  const abs = join17(projectPath, rel);
7999
- const s = await stat10(abs).catch(() => null);
8984
+ const s = await stat11(abs).catch(() => null);
8000
8985
  if (!s) return { error: tr("fs.fileMissing") };
8001
8986
  if (!s.isFile()) return { error: tr("fs.notFile") };
8002
8987
  if (s.size > MAX_READ) return { tooLarge: true };
@@ -8014,7 +8999,7 @@ init_pathscan();
8014
8999
  init_log();
8015
9000
  init_i18n();
8016
9001
  import { spawn as spawn3, execFileSync as execFileSync2 } from "node:child_process";
8017
- import { accessSync as accessSync2, chmodSync, constants as fsc, existsSync as existsSync11, statSync as statSync6 } from "node:fs";
9002
+ import { accessSync as accessSync2, chmodSync, constants as fsc, existsSync as existsSync12, statSync as statSync6 } from "node:fs";
8018
9003
  import { createRequire as moduleRequire } from "node:module";
8019
9004
  import { release } from "node:os";
8020
9005
  import { dirname as dirname6, join as join18 } from "node:path";
@@ -8064,7 +9049,7 @@ function fixSpawnHelper() {
8064
9049
  const root = dirname6(req.resolve("node-pty/package.json"));
8065
9050
  for (const sub of [`prebuilds/darwin-${process.arch}`, "build/Release"]) {
8066
9051
  const f = join18(root, sub, "spawn-helper");
8067
- if (existsSync11(f) && !isExecFile(f)) {
9052
+ if (existsSync12(f) && !isExecFile(f)) {
8068
9053
  chmodSync(f, 493);
8069
9054
  log("info", "\u5DF2\u4FEE\u590D node-pty spawn-helper \u7684\u53EF\u6267\u884C\u4F4D", { file: f });
8070
9055
  }
@@ -8296,9 +9281,9 @@ function pidAlive(pid, kill = (p, s) => process.kill(p, s)) {
8296
9281
  }
8297
9282
  function augmentPath(env, nodeDir, platform = process.platform, home) {
8298
9283
  const win = platform === "win32";
8299
- const sep4 = win ? ";" : platform === process.platform ? delimiter : ":";
9284
+ const sep5 = win ? ";" : platform === process.platform ? delimiter : ":";
8300
9285
  const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
8301
- const cur = (env[key] ?? "").split(sep4).filter(Boolean);
9286
+ const cur = (env[key] ?? "").split(sep5).filter(Boolean);
8302
9287
  const norm = (d) => win ? d.replace(/[\\/]+$/, "").toLowerCase() : d.replace(/\/+$/, "");
8303
9288
  const seen = new Set(cur.map(norm));
8304
9289
  const out = [...cur];
@@ -8307,7 +9292,87 @@ function augmentPath(env, nodeDir, platform = process.platform, home) {
8307
9292
  seen.add(norm(d));
8308
9293
  out.push(d);
8309
9294
  }
8310
- return out.join(sep4);
9295
+ return out.join(sep5);
9296
+ }
9297
+
9298
+ // daemon/src/main.ts
9299
+ init_proxy();
9300
+
9301
+ // daemon/src/preview/ports.ts
9302
+ import { readFileSync as readFileSync2 } from "node:fs";
9303
+ import { homedir as homedir9 } from "node:os";
9304
+ import { basename as basename5, dirname as dirname7, isAbsolute as isAbsolute3, join as join19 } from "node:path";
9305
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
9306
+ var MAGPIE_DEFAULT_PORTS = [3425, 3430];
9307
+ var GATE_DEFAULT_PORT = 3488;
9308
+ function portOf(addr) {
9309
+ if (!addr) return null;
9310
+ const m = /:(\d{1,5})\/?$/.exec(addr.trim());
9311
+ const n = m ? Number(m[1]) : NaN;
9312
+ return Number.isInteger(n) && n > 0 && n <= 65535 ? n : null;
9313
+ }
9314
+ function magpieDirFromInstall(p) {
9315
+ const norm = p.replace(/\\/g, "/");
9316
+ const i = norm.lastIndexOf("/plugins/node_modules/");
9317
+ if (i <= 0) return null;
9318
+ return p.slice(0, i);
9319
+ }
9320
+ function magpieConfigDirs(env = process.env, where = {}) {
9321
+ const out = [];
9322
+ const add = (d) => {
9323
+ if (d && isAbsolute3(d) && !out.includes(d)) out.push(d);
9324
+ };
9325
+ let self = where.self;
9326
+ if (self === void 0) {
9327
+ try {
9328
+ self = fileURLToPath3(import.meta.url);
9329
+ } catch {
9330
+ self = "";
9331
+ }
9332
+ }
9333
+ add(magpieDirFromInstall(self));
9334
+ const cwd = where.cwd ?? (env === process.env ? process.cwd() : "");
9335
+ if (cwd && basename5(cwd) === "plugin-work") add(dirname7(cwd));
9336
+ const x = env.XDG_CONFIG_HOME;
9337
+ if (x && isAbsolute3(x)) add(join19(x, "magpie"));
9338
+ const platform = where.platform ?? process.platform;
9339
+ const home = where.home ?? (env === process.env ? homedir9() : platform === "win32" ? env.USERPROFILE : env.HOME);
9340
+ if (home) add(join19(home, ".config", "magpie"));
9341
+ return out;
9342
+ }
9343
+ function savedGatewayPort(file, read = (f) => readFileSync2(f, "utf8")) {
9344
+ let raw;
9345
+ try {
9346
+ raw = read(file);
9347
+ } catch {
9348
+ return null;
9349
+ }
9350
+ try {
9351
+ const s = JSON.parse(raw);
9352
+ const p = typeof s?.port === "number" ? s.port : 0;
9353
+ return Number.isInteger(p) && p >= 1024 && p <= 65535 ? p : MAGPIE_DEFAULT_PORTS[0];
9354
+ } catch {
9355
+ return null;
9356
+ }
9357
+ }
9358
+ function previewDeniedPorts(cfg2, env = process.env, opts = {}) {
9359
+ const out = /* @__PURE__ */ new Set([cfg2.port, GATE_DEFAULT_PORT, ...MAGPIE_DEFAULT_PORTS]);
9360
+ if (cfg2.gatePort) out.add(cfg2.gatePort);
9361
+ for (const k of ["MAGPIE_ADDR", "MAGPIE_WEB_ADDR", "MAGPIE_URL"]) {
9362
+ const p = portOf(env[k]);
9363
+ if (p) out.add(p);
9364
+ }
9365
+ for (const d of opts.configDirs ?? magpieConfigDirs(env)) {
9366
+ const p = savedGatewayPort(join19(d, "settings.json"), opts.read);
9367
+ if (p) out.add(p);
9368
+ }
9369
+ const extra = env.AGENT_CONSOLE_PREVIEW_DENY;
9370
+ if (extra) for (const s of extra.split(/[,\s]+/)) {
9371
+ const n = Number(s);
9372
+ if (Number.isInteger(n) && n > 0 && n <= 65535) out.add(n);
9373
+ }
9374
+ out.delete(0);
9375
+ return [...out];
8311
9376
  }
8312
9377
 
8313
9378
  // daemon/src/main.ts
@@ -8316,7 +9381,7 @@ if (nodeMajor < 20) {
8316
9381
  console.error(`[agent-console] \u9700\u8981 Node.js 20 \u6216\u66F4\u9AD8\u7248\u672C\uFF0C\u5F53\u524D ${process.version}\uFF08${process.execPath}\uFF09`);
8317
9382
  process.exit(3);
8318
9383
  }
8319
- process.env.PATH = augmentPath(process.env, dirname7(process.execPath));
9384
+ process.env.PATH = augmentPath(process.env, dirname8(process.execPath));
8320
9385
  var cfg = parseConfig(process.argv.slice(2));
8321
9386
  process.on("uncaughtException", (e) => {
8322
9387
  if (e?.code === "EPIPE") return;
@@ -8328,10 +9393,10 @@ process.on("unhandledRejection", (e) => {
8328
9393
  async function main() {
8329
9394
  initLog(cfg.dataDir);
8330
9395
  for (const d of ["threads", "attachments", "trash", "logs"]) {
8331
- mkdirSync2(join20(cfg.dataDir, d), { recursive: true });
9396
+ mkdirSync2(join21(cfg.dataDir, d), { recursive: true });
8332
9397
  }
8333
- const appDir = join20(dirname7(fileURLToPath3(import.meta.url)), "..", "..", "app");
8334
- const attachments = new AttachmentStore(join20(cfg.dataDir, "attachments"));
9398
+ const appDir = join21(dirname8(fileURLToPath4(import.meta.url)), "..", "..", "app");
9399
+ const attachments = new AttachmentStore(join21(cfg.dataDir, "attachments"));
8335
9400
  const { bindAttachmentStore: bindAttachmentStore2 } = await Promise.resolve().then(() => (init_attachmentRefs(), attachmentRefs_exports));
8336
9401
  bindAttachmentStore2(attachments);
8337
9402
  const settings = new SettingsStore(cfg.dataDir);
@@ -8348,6 +9413,13 @@ async function main() {
8348
9413
  const manager = new AgentManager(gateway, threads, projects, settings, attachments);
8349
9414
  await manager.recoverOnStart();
8350
9415
  const terminals = new TerminalService(gateway);
9416
+ const preview = new PreviewProxy({
9417
+ deniedPorts: () => previewDeniedPorts(cfg),
9418
+ log: (level, msg) => log(level, msg)
9419
+ });
9420
+ gateway.onConnectionClosed = (left) => {
9421
+ if (left === 0) preview.closeAll();
9422
+ };
8351
9423
  registerMethods(gateway, {
8352
9424
  cfg,
8353
9425
  settings,
@@ -8358,6 +9430,7 @@ async function main() {
8358
9430
  git: new GitService(),
8359
9431
  files: new FileService(),
8360
9432
  terminals,
9433
+ preview,
8361
9434
  runtime: (id2) => manager.runtimeInfo(id2)
8362
9435
  });
8363
9436
  const server = createHttpServer(cfg, appDir, attachments, {
@@ -8390,7 +9463,7 @@ async function main() {
8390
9463
  const { warmProviderCache: warmProviderCache2 } = await Promise.resolve().then(() => (init_warm(), warm_exports));
8391
9464
  await warmProviderCache2(await settings.get());
8392
9465
  })().catch((e) => log("warn", "Provider \u9884\u70ED\u5931\u8D25", { err: String(e) }));
8393
- const daemonJsonPath = join20(cfg.dataDir, "daemon.json");
9466
+ const daemonJsonPath = join21(cfg.dataDir, "daemon.json");
8394
9467
  await writeJsonAtomic(daemonJsonPath, {
8395
9468
  pid: process.pid,
8396
9469
  port: cfg.port,
@@ -8419,6 +9492,7 @@ async function main() {
8419
9492
  shuttingDown = true;
8420
9493
  log("info", "daemon \u5173\u95ED\u4E2D");
8421
9494
  terminals.killAll();
9495
+ preview.closeAll();
8422
9496
  gateway.closeAll();
8423
9497
  try {
8424
9498
  rmSync2(daemonJsonPath, { force: true });