@decentnetwork/lan 0.1.212 → 0.1.215

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.
@@ -92,6 +92,11 @@ export declare class PeerManager extends EventEmitter {
92
92
  acceptFile(userid: string, fileNumber: number): void;
93
93
  /** Cancel an in-flight send by content fileId. Returns true if it was found. */
94
94
  cancelSend(userid: string, fileId: string): boolean;
95
+ /** Cancel in-flight sends matching size/name when activeSends lost the fileId. */
96
+ cancelSendsMatching(userid: string, match: {
97
+ name?: string;
98
+ size: number;
99
+ }): string[];
95
100
  /**
96
101
  * Send an outbound friend request to a Carrier address (NOT a bare
97
102
  * userid — sendFriendRequest needs the address form because it
@@ -209,6 +209,10 @@ export class PeerManager extends EventEmitter {
209
209
  cancelSend(userid, fileId) {
210
210
  return this.peer?.cancelFileById(userid, fileId, true) ?? false;
211
211
  }
212
+ /** Cancel in-flight sends matching size/name when activeSends lost the fileId. */
213
+ cancelSendsMatching(userid, match) {
214
+ return this.peer?.cancelSendsMatching(userid, match) ?? [];
215
+ }
212
216
  /**
213
217
  * Send an outbound friend request to a Carrier address (NOT a bare
214
218
  * userid — sendFriendRequest needs the address form because it
@@ -543,16 +543,52 @@ export class DaemonServer {
543
543
  return { deleted: removed.length, files };
544
544
  },
545
545
  fileCancel: async (userid, ids) => {
546
+ // Cancel must ACTUALLY stop the peer pump. An earlier path set
547
+ // stopped=true after only deleting activeSends even when cancelSend
548
+ // returned false — UI said cancelled, file-cc kept running, and the
549
+ // next send looked dead. Try the active registry, content hash, then
550
+ // an exact name+size peer fallback before reporting success/failure.
551
+ const fs = await import("fs/promises");
552
+ const { createHash } = await import("crypto");
546
553
  let n = 0;
547
554
  for (const id of ids) {
548
- for (const [fid, v] of this.activeSends) {
549
- if (v.peer !== userid || v.msgId !== id)
550
- continue;
555
+ const msg = this.messageStore?.get(userid, id);
556
+ const wasActive = msg?.file?.status === "sending" || msg?.file?.status === "queued";
557
+ let killed = false;
558
+ const tryKill = (fid) => {
559
+ if (!fid)
560
+ return;
551
561
  if (this.peerManager?.cancelSend(userid, fid))
552
- n++;
562
+ killed = true;
553
563
  this.activeSends.delete(fid);
564
+ };
565
+ for (const [fid, v] of [...this.activeSends]) {
566
+ if (v.peer !== userid || v.msgId !== id)
567
+ continue;
568
+ tryKill(fid);
569
+ }
570
+ try {
571
+ const data = await fs.readFile(resolve(this.outboxDir, id));
572
+ tryKill(createHash("sha256").update(data).digest("hex"));
573
+ }
574
+ catch {
575
+ // outbox entry may already be gone
576
+ }
577
+ if (msg?.file) {
578
+ const matched = this.peerManager?.cancelSendsMatching(userid, {
579
+ name: msg.file.name,
580
+ size: msg.file.size,
581
+ }) ?? [];
582
+ for (const fid of matched)
583
+ this.activeSends.delete(fid);
584
+ if (matched.length)
585
+ killed = true;
586
+ }
587
+ if (killed || wasActive) {
554
588
  this.messageStore?.patchFile(userid, id, { status: "failed", sent: 0, kbps: undefined });
589
+ n++;
555
590
  }
591
+ this.logger.info(`file-cancel peer=${userid.slice(0, 8)} msg=${id} killed=${killed ? 1 : 0} wasActive=${wasActive ? 1 : 0}`);
556
592
  }
557
593
  this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
558
594
  this.logger.info(`Cancelled ${n} send(s) for ${userid.slice(0, 8)}`);
@@ -16,6 +16,7 @@ const ICON_PATHS = {
16
16
  checkCheck: '<path d="M18 6 7 17l-5-5"/><path d="m22 10-7.6 7.6L13 16"/>',
17
17
  copy: '<rect x="9" y="9" width="12" height="12" rx="2.4"/><path d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"/>',
18
18
  refresh: '<path d="M21 12a9 9 0 1 1-2.6-6.3"/><path d="M21 4v4.5h-4.5"/>',
19
+ clock: '<circle cx="12" cy="12" r="9.5"/><path d="M12 7.5V12l3 2"/>',
19
20
  edit: '<path d="M11 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4Z"/>',
20
21
  qr: '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><path d="M14 14h3v3M21 14v.01M14 21h.01M17 21h.01M21 17v4"/>',
21
22
  // ---- app tiles ----
@@ -1072,7 +1073,7 @@ const mediaBtnStyle = {
1072
1073
  justifyContent: "center",
1073
1074
  lineHeight: 0
1074
1075
  };
1075
- function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, selected, onToggleSel }) {
1076
+ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, selected, onToggleSel, busy }) {
1076
1077
  const mine = m.from === "me";
1077
1078
  return /* @__PURE__ */ React.createElement(
1078
1079
  "div",
@@ -1169,28 +1170,31 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, sele
1169
1170
  cursor: mine ? "default" : "pointer"
1170
1171
  };
1171
1172
  const icon = /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 7, flexShrink: 0, background: mine ? "rgba(255,255,255,0.16)" : "var(--chip)", display: "flex", alignItems: "center", justifyContent: "center", color: mine ? "#fff" : "var(--accent)" } }, /* @__PURE__ */ React.createElement(Icon, { name: m.file.media === "image" ? "image" : m.file.media === "video" ? "video" : m.file.media === "audio" ? "play" : "file", size: 18, stroke: 1.9 }));
1172
- const meta = /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: mine ? "#fff" : "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: mine ? "rgba(255,255,255,0.7)" : "var(--faint)", marginTop: 1 } }, m.file.status === "queued" ? `${m.file.size} \xB7 ${T.queued || "queued"}` : m.file.status === "sending" ? `${m.file.size} \xB7 ${dkPct(m.file.pct)}${m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : ""}` : m.file.status === "failed" ? `${m.file.size} \xB7 failed` : mine ? `${m.file.size} \xB7 sent` : `${m.file.size} \xB7 ${T.open || "open"}`));
1173
+ const meta = /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: mine ? "#fff" : "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: mine ? "rgba(255,255,255,0.7)" : "var(--faint)", marginTop: 1 } }, busy === "cancel" ? `${m.file.size} \xB7 ${T.cancelling || "Cancelling\u2026"}` : busy === "retry" ? `${m.file.size} \xB7 ${T.retrying || "Retrying\u2026"}` : m.file.status === "queued" ? `${m.file.size} \xB7 ${T.queued || "queued"}` : m.file.status === "sending" ? `${m.file.size} \xB7 ${dkPct(m.file.pct)}${m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : ""}` : m.file.status === "failed" ? `${m.file.size} \xB7 failed` : mine ? `${m.file.size} \xB7 sent` : `${m.file.size} \xB7 ${T.open || "open"}`));
1173
1174
  if (mine) {
1174
- return /* @__PURE__ */ React.createElement("div", { style: cardStyle }, icon, meta, m.file.status === "queued" ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : m.file.status === "sending" ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "rgba(255,255,255,0.9)", whiteSpace: "nowrap" } }, dkPct(m.file.pct), m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : "") : m.file.status === "failed" ? /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 16, stroke: 2.4, color: "rgba(255,200,190,0.95)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }), (m.file.status === "sending" || m.file.status === "queued") && m.id && /* @__PURE__ */ React.createElement(
1175
+ const btnBusy = !!busy;
1176
+ return /* @__PURE__ */ React.createElement("div", { style: { ...cardStyle, opacity: btnBusy ? 0.85 : 1 } }, icon, meta, busy === "cancel" || busy === "retry" ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : m.file.status === "queued" ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : m.file.status === "sending" ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "rgba(255,255,255,0.9)", whiteSpace: "nowrap" } }, dkPct(m.file.pct), m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : "") : m.file.status === "failed" ? /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 16, stroke: 2.4, color: "rgba(255,200,190,0.95)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }), (m.file.status === "sending" || m.file.status === "queued" || busy === "cancel") && m.id && /* @__PURE__ */ React.createElement(
1175
1177
  "button",
1176
1178
  {
1177
1179
  title: T.cancel || "cancel",
1180
+ disabled: btnBusy,
1178
1181
  onClick: (e) => {
1179
1182
  e.stopPropagation();
1180
- onCancel(m.id);
1183
+ if (!btnBusy) onCancel(m.id);
1181
1184
  },
1182
- style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0 }
1185
+ style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: btnBusy ? "wait" : "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0, opacity: btnBusy ? 0.5 : 1 }
1183
1186
  },
1184
1187
  /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 13, stroke: 2.4, color: "#fff" })
1185
1188
  ), m.file.status !== "sent" && m.id && /* @__PURE__ */ React.createElement(
1186
1189
  "button",
1187
1190
  {
1188
1191
  title: T.retry || "retry",
1192
+ disabled: btnBusy,
1189
1193
  onClick: (e) => {
1190
1194
  e.stopPropagation();
1191
- onRetry(m.id);
1195
+ if (!btnBusy) onRetry(m.id);
1192
1196
  },
1193
- style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0 }
1197
+ style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: btnBusy ? "wait" : "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0, opacity: btnBusy ? 0.5 : 1 }
1194
1198
  },
1195
1199
  /* @__PURE__ */ React.createElement(Icon, { name: "refresh", size: 13, stroke: 2.2, color: "#fff" })
1196
1200
  ));
@@ -1254,36 +1258,130 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, sele
1254
1258
  ), mine && m.status === "queued" ? /* @__PURE__ */ React.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 2 }, title: "waiting for peer \u2014 will send when they're online" }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--faint)" }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, T.queued || "queued")) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
1255
1259
  );
1256
1260
  }
1257
- function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall }) {
1261
+ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
1258
1262
  const scrollRef = React.useRef(null);
1259
1263
  const fileRef = React.useRef(null);
1260
- const thread = threadProp && threadProp.length ? threadProp : [{ day: "Today" }, { from: "them", time: "\u2014", text: lang === "zh" ? "\u6682\u65E0\u6D88\u606F\u8BB0\u5F55\uFF0C\u53D1\u4E2A\u6D88\u606F\u6253\u4E2A\u62DB\u547C\u5427\u3002" : "No messages yet. Say hi." }];
1261
- React.useEffect(() => {
1262
- if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
1263
- }, [peer.id, threadProp]);
1264
1264
  const [menu, setMenu] = React.useState(false);
1265
1265
  const [selMode, setSelMode] = React.useState(false);
1266
1266
  const [sel, setSel] = React.useState(() => /* @__PURE__ */ new Set());
1267
1267
  const [hidden, setHidden] = React.useState(() => /* @__PURE__ */ new Set());
1268
+ const [fileBusy, setFileBusy] = React.useState(() => ({}));
1269
+ const [filePatch, setFilePatch] = React.useState(() => ({}));
1270
+ const [flash, setFlash] = React.useState(null);
1271
+ const flashTimer = React.useRef(null);
1272
+ const showFlash = (kind, msg) => {
1273
+ if (flashTimer.current) clearTimeout(flashTimer.current);
1274
+ setFlash({ kind, msg });
1275
+ flashTimer.current = setTimeout(() => setFlash(null), 2500);
1276
+ };
1277
+ React.useEffect(() => () => {
1278
+ if (flashTimer.current) clearTimeout(flashTimer.current);
1279
+ }, []);
1268
1280
  React.useEffect(() => {
1269
1281
  setSelMode(false);
1270
1282
  setSel(/* @__PURE__ */ new Set());
1271
1283
  setHidden(/* @__PURE__ */ new Set());
1284
+ setFileBusy({});
1285
+ setFilePatch({});
1286
+ setFlash(null);
1272
1287
  }, [peer.id]);
1288
+ React.useEffect(() => {
1289
+ setFilePatch((p) => {
1290
+ let changed = false;
1291
+ const next = { ...p };
1292
+ for (const m of threadProp || []) {
1293
+ if (!m || !m.id || !next[m.id] || !m.file) continue;
1294
+ if (m.file.status === next[m.id].status) {
1295
+ delete next[m.id];
1296
+ changed = true;
1297
+ }
1298
+ }
1299
+ return changed ? next : p;
1300
+ });
1301
+ }, [threadProp]);
1302
+ const baseThread = threadProp && threadProp.length ? threadProp : [{ day: "Today" }, { from: "them", time: "\u2014", text: lang === "zh" ? "\u6682\u65E0\u6D88\u606F\u8BB0\u5F55\uFF0C\u53D1\u4E2A\u6D88\u606F\u6253\u4E2A\u62DB\u547C\u5427\u3002" : "No messages yet. Say hi." }];
1303
+ const thread = React.useMemo(() => {
1304
+ if (!Object.keys(filePatch).length) return baseThread;
1305
+ return baseThread.map((m) => {
1306
+ if (!m || !m.id || !m.file || !filePatch[m.id]) return m;
1307
+ return { ...m, file: { ...m.file, ...filePatch[m.id] } };
1308
+ });
1309
+ }, [baseThread, filePatch]);
1310
+ React.useEffect(() => {
1311
+ if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
1312
+ }, [peer.id, threadProp]);
1273
1313
  const toggleSel = (id) => setSel((s) => {
1274
1314
  const n = new Set(s);
1275
1315
  n.has(id) ? n.delete(id) : n.add(id);
1276
1316
  return n;
1277
1317
  });
1318
+ const clearBusy = (id) => setFileBusy((b) => {
1319
+ const n = { ...b };
1320
+ delete n[id];
1321
+ return n;
1322
+ });
1278
1323
  const doCancel = (id) => {
1279
- if (id) dkApi.cancelSend(peer.userId, [id]).catch(() => {
1280
- });
1324
+ if (!id || fileBusy[id]) return;
1325
+ setFileBusy((b) => ({ ...b, [id]: "cancel" }));
1326
+ showFlash("ok", T.cancelling || "Cancelling\u2026");
1327
+ dkApi.cancelSend(peer.userId, [id]).then((r) => {
1328
+ if (!r || r.ok === false) {
1329
+ const msg = (T.cancelFailed || "Cancel failed") + ": " + (r && r.error || "daemon unavailable");
1330
+ showFlash("err", msg);
1331
+ window.alert(msg);
1332
+ } else if (!r.cancelled) {
1333
+ const msg = T.cancelMissing || "No active transfer was found. Refresh and try again.";
1334
+ showFlash("err", msg);
1335
+ window.alert(msg);
1336
+ } else {
1337
+ setFilePatch((p) => ({ ...p, [id]: { status: "failed", pct: void 0, kbps: void 0 } }));
1338
+ showFlash("ok", T.cancelled || "Transfer cancelled");
1339
+ if (onReloadThread) onReloadThread();
1340
+ }
1341
+ }).catch((e) => {
1342
+ const msg = (T.cancelFailed || "Cancel failed") + ": " + String(e);
1343
+ showFlash("err", msg);
1344
+ window.alert(msg);
1345
+ }).finally(() => clearBusy(id));
1281
1346
  };
1282
1347
  const doRetry = (id) => {
1283
- if (!id) return;
1348
+ if (!id || fileBusy[id]) return;
1349
+ setFileBusy((b) => ({ ...b, [id]: "retry" }));
1350
+ setFilePatch((p) => ({ ...p, [id]: { status: "sending", pct: 0, kbps: void 0 } }));
1351
+ showFlash("ok", T.retrying || "Retrying\u2026");
1284
1352
  dkApi.retrySend(peer.userId, [id]).then((r) => {
1285
- if (r && r.ok === false) window.alert((lang === "zh" ? "\u91CD\u8BD5\u5931\u8D25: " : "Retry failed: ") + (r.error || ""));
1286
- });
1353
+ if (r && r.ok === false) {
1354
+ setFilePatch((p) => {
1355
+ const n = { ...p };
1356
+ delete n[id];
1357
+ return n;
1358
+ });
1359
+ const msg = (T.retryFailed || "Retry failed") + ": " + (r.error || "");
1360
+ showFlash("err", msg);
1361
+ window.alert(msg);
1362
+ } else if (!r || !r.retried) {
1363
+ setFilePatch((p) => {
1364
+ const n = { ...p };
1365
+ delete n[id];
1366
+ return n;
1367
+ });
1368
+ const msg = T.retryMissing || "No retryable transfer or original file is unavailable.";
1369
+ showFlash("err", msg);
1370
+ window.alert(msg);
1371
+ } else {
1372
+ showFlash("ok", T.retried || "Transfer restarted");
1373
+ if (onReloadThread) onReloadThread();
1374
+ }
1375
+ }).catch((e) => {
1376
+ setFilePatch((p) => {
1377
+ const n = { ...p };
1378
+ delete n[id];
1379
+ return n;
1380
+ });
1381
+ const msg = (T.retryFailed || "Retry failed") + ": " + String(e);
1382
+ showFlash("err", msg);
1383
+ window.alert(msg);
1384
+ }).finally(() => clearBusy(id));
1287
1385
  };
1288
1386
  const doDelete = (ids) => {
1289
1387
  ids = ids.filter(Boolean);
@@ -1371,6 +1469,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
1371
1469
  onDelete: (id) => doDelete([id]),
1372
1470
  onCancel: doCancel,
1373
1471
  onRetry: doRetry,
1472
+ busy: m.id ? fileBusy[m.id] : void 0,
1374
1473
  selMode,
1375
1474
  selected: sel.has(m.id),
1376
1475
  onToggleSel: toggleSel
@@ -1380,7 +1479,24 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
1380
1479
  setSelMode(false);
1381
1480
  setSel(/* @__PURE__ */ new Set());
1382
1481
  } }, T.cancel || "Cancel"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", icon: "trash", onClick: () => doDelete([...sel]) }, T.delete || "Delete")),
1383
- /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: "12px 16px", background: "var(--panel)" } }, sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto 8px", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "paperclip", size: 13, stroke: 2, color: "var(--accent)" }), (lang === "zh" ? "\u53D1\u9001\u4E2D: " : "Sending: ") + sending.name), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
1482
+ /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: "12px 16px", background: "var(--panel)" } }, flash && /* @__PURE__ */ React.createElement("div", { style: {
1483
+ maxWidth: 760,
1484
+ margin: "0 auto 8px",
1485
+ display: "flex",
1486
+ alignItems: "center",
1487
+ gap: 8,
1488
+ fontFamily: "var(--mono)",
1489
+ fontSize: 11.5,
1490
+ color: flash.kind === "err" ? "var(--danger)" : "var(--accent)"
1491
+ } }, /* @__PURE__ */ React.createElement(
1492
+ Icon,
1493
+ {
1494
+ name: flash.kind === "err" ? "x" : "check",
1495
+ size: 13,
1496
+ stroke: 2.2,
1497
+ color: flash.kind === "err" ? "var(--danger)" : "var(--accent)"
1498
+ }
1499
+ ), flash.msg), sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto 8px", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "paperclip", size: 13, stroke: 2, color: "var(--accent)" }), (lang === "zh" ? "\u53D1\u9001\u4E2D: " : "Sending: ") + sending.name), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
1384
1500
  "input",
1385
1501
  {
1386
1502
  ref: fileRef,
@@ -1475,9 +1591,9 @@ function MenuItem({ icon, label, onClick, danger }) {
1475
1591
  function ChatEmpty({ T }) {
1476
1592
  return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, color: "var(--faint)", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "message", size: 40, stroke: 1.4, color: "var(--line)" }), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13 } }, T.pickPeer));
1477
1593
  }
1478
- function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall }) {
1594
+ function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
1479
1595
  const peer = peers.find((p) => p.id === activeId);
1480
- return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
1596
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
1481
1597
  }
1482
1598
  Object.assign(window, { ChatTab });
1483
1599
  function StatTile({ label, value, sub, tone }) {
@@ -1977,6 +2093,14 @@ const STR = {
1977
2093
  open: "open",
1978
2094
  retry: "Retry",
1979
2095
  cancel: "Cancel",
2096
+ cancelling: "Cancelling\u2026",
2097
+ cancelled: "Transfer cancelled",
2098
+ retrying: "Retrying\u2026",
2099
+ retried: "Transfer restarted",
2100
+ cancelFailed: "Cancel failed",
2101
+ retryFailed: "Retry failed",
2102
+ cancelMissing: "No active transfer was found. Refresh and try again.",
2103
+ retryMissing: "No retryable transfer or original file is unavailable.",
1980
2104
  myIp: "my ip",
1981
2105
  copyAddr: "copy address",
1982
2106
  peersOnline: "peers online",
@@ -2059,6 +2183,14 @@ const STR = {
2059
2183
  open: "\u6253\u5F00",
2060
2184
  retry: "\u91CD\u8BD5",
2061
2185
  cancel: "\u53D6\u6D88",
2186
+ cancelling: "\u6B63\u5728\u53D6\u6D88\u2026",
2187
+ cancelled: "\u5DF2\u53D6\u6D88\u4F20\u8F93",
2188
+ retrying: "\u6B63\u5728\u91CD\u8BD5\u2026",
2189
+ retried: "\u5DF2\u91CD\u65B0\u5F00\u59CB\u4F20\u8F93",
2190
+ cancelFailed: "\u53D6\u6D88\u5931\u8D25",
2191
+ retryFailed: "\u91CD\u8BD5\u5931\u8D25",
2192
+ cancelMissing: "\u6CA1\u6709\u627E\u5230\u6B63\u5728\u8FDB\u884C\u7684\u4F20\u8F93\uFF1B\u8BF7\u5237\u65B0\u9875\u9762\u540E\u91CD\u8BD5\u3002",
2193
+ retryMissing: "\u6CA1\u6709\u53EF\u91CD\u8BD5\u7684\u4F20\u8F93\uFF0C\u6216\u539F\u6587\u4EF6\u5DF2\u4E0D\u53EF\u7528\u3002",
2062
2194
  myIp: "\u6211\u7684 IP",
2063
2195
  copyAddr: "\u590D\u5236\u5730\u5740",
2064
2196
  peersOnline: "\u5728\u7EBF\u597D\u53CB",
@@ -2206,7 +2338,7 @@ function DkApp() {
2206
2338
  { id: "network", icon: "network", label: T.network },
2207
2339
  { id: "profile", icon: "userRound", label: T.profile }
2208
2340
  ];
2209
- return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
2341
+ return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId) }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
2210
2342
  TweakRadio,
2211
2343
  {
2212
2344
  label: t.lang === "zh" ? "\u4E3B\u9898" : "Theme",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.212",
3
+ "version": "0.1.215",
4
4
  "description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -84,7 +84,7 @@
84
84
  },
85
85
  "dependencies": {
86
86
  "@decentnetwork/dora": "^0.1.14",
87
- "@decentnetwork/peer": "^0.1.105",
87
+ "@decentnetwork/peer": "^0.1.108",
88
88
  "@decentnetwork/peer-webrtc": "^0.2.10",
89
89
  "ink": "^5.2.1",
90
90
  "js-yaml": "^4.1.0",