@elefunc/send 0.1.18 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elefunc/send",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Browser-compatible file transfer CLI and TUI powered by Bun, WebRTC, and Rezi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/core/files.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { access, mkdir, open, rm, stat, type FileHandle } from "node:fs/promises"
1
+ import { access, mkdir, open, rename, rm, stat, type FileHandle } from "node:fs/promises"
2
2
  import { basename, extname, join, resolve } from "node:path"
3
3
  import { resolveUserPath } from "./paths"
4
4
 
@@ -101,8 +101,23 @@ export const uniqueOutputPath = async (directory: string, fileName: string, rese
101
101
  }
102
102
  }
103
103
 
104
- export const saveIncomingFile = async (directory: string, fileName: string, data: Buffer) => {
105
- const path = await uniqueOutputPath(directory, fileName)
104
+ export const incomingOutputPath = async (directory: string, fileName: string, overwrite = false, reservedPaths: ReadonlySet<string> = new Set()) => {
105
+ await mkdir(directory, { recursive: true })
106
+ return overwrite ? join(directory, fileName) : uniqueOutputPath(directory, fileName, reservedPaths)
107
+ }
108
+
109
+ export const replaceOutputPath = async (sourcePath: string, destinationPath: string) => {
110
+ try {
111
+ await rename(sourcePath, destinationPath)
112
+ } catch (error) {
113
+ if (!await pathExists(destinationPath)) throw error
114
+ await removePath(destinationPath)
115
+ await rename(sourcePath, destinationPath)
116
+ }
117
+ }
118
+
119
+ export const saveIncomingFile = async (directory: string, fileName: string, data: Buffer, overwrite = false) => {
120
+ const path = await incomingOutputPath(directory, fileName, overwrite)
106
121
  await Bun.write(path, data)
107
122
  return path
108
123
  }
@@ -2,7 +2,7 @@ import { open, rename, type FileHandle } from "node:fs/promises"
2
2
  import { resolve } from "node:path"
3
3
  import type { RTCDataChannel, RTCIceCandidateInit, RTCIceServer } from "werift"
4
4
  import { RTCPeerConnection } from "werift"
5
- import { closeLocalFile, loadLocalFiles, pathExists, readFileChunk, removePath, saveIncomingFile, uniqueOutputPath, writeFileChunk, type LocalFile } from "./files"
5
+ import { closeLocalFile, incomingOutputPath, loadLocalFiles, pathExists, readFileChunk, removePath, replaceOutputPath, saveIncomingFile, uniqueOutputPath, writeFileChunk, type LocalFile } from "./files"
6
6
  import {
7
7
  BASE_ICE_SERVERS,
8
8
  BUFFER_HIGH,
@@ -176,6 +176,7 @@ export interface SessionConfig {
176
176
  peerSelectionMemory?: Map<string, boolean>
177
177
  autoAcceptIncoming?: boolean
178
178
  autoSaveIncoming?: boolean
179
+ overwriteIncoming?: boolean
179
180
  turnUrls?: string[]
180
181
  turnUsername?: string
181
182
  turnCredential?: string
@@ -184,6 +185,8 @@ export interface SessionConfig {
184
185
 
185
186
  const LOG_LIMIT = 200
186
187
  const STATS_POLL_MS = 1000
188
+ export const PULSE_PROBE_INTERVAL_MS = 15_000
189
+ export const PULSE_STALE_MS = 45_000
187
190
  const PROFILE_URL = "https://ip.rt.ht/"
188
191
  export interface PeerConnectivitySnapshot {
189
192
  rttMs: number
@@ -194,9 +197,12 @@ export interface PeerConnectivitySnapshot {
194
197
 
195
198
  export type TurnState = "none" | "idle" | "used"
196
199
  export type PulseState = "idle" | "checking" | "open" | "error"
200
+ export type SettledPulseState = "idle" | "open" | "error"
201
+ export type SignalMetricState = "idle" | "connecting" | "checking" | "open" | "degraded" | "closed" | "error"
197
202
 
198
203
  export interface PulseSnapshot {
199
204
  state: PulseState
205
+ lastSettledState: SettledPulseState
200
206
  at: number
201
207
  ms: number
202
208
  error: string
@@ -206,7 +212,27 @@ const progressOf = (transfer: TransferState) => transfer.size ? Math.max(0, Math
206
212
  const isFinal = (transfer: TransferState) => FINAL_STATUSES.has(transfer.status as never)
207
213
  export const candidateTypeLabel = (type: string) => ({ host: "Direct", srflx: "NAT", prflx: "NAT", relay: "TURN" }[type] || "—")
208
214
  const emptyConnectivitySnapshot = (): PeerConnectivitySnapshot => ({ rttMs: Number.NaN, localCandidateType: "", remoteCandidateType: "", pathLabel: "—" })
209
- const emptyPulseSnapshot = (): PulseSnapshot => ({ state: "idle", at: 0, ms: 0, error: "" })
215
+ const settledPulseState = (pulse?: Pick<PulseSnapshot, "state" | "lastSettledState"> | null): SettledPulseState =>
216
+ pulse?.lastSettledState === "open" || pulse?.lastSettledState === "error" || pulse?.lastSettledState === "idle"
217
+ ? pulse.lastSettledState
218
+ : pulse?.state === "open"
219
+ ? "open"
220
+ : pulse?.state === "error"
221
+ ? "error"
222
+ : "idle"
223
+ const pulseIsFresh = (pulse: Pick<PulseSnapshot, "at" | "lastSettledState" | "state">, now = Date.now()) =>
224
+ settledPulseState(pulse) === "open" && Number.isFinite(pulse.at) && pulse.at > 0 && now - pulse.at <= PULSE_STALE_MS
225
+ export const signalMetricState = (socketState: SocketState, pulse: PulseSnapshot, now = Date.now()): SignalMetricState => {
226
+ if (socketState === "error") return "error"
227
+ if (socketState === "closed") return "closed"
228
+ if (socketState === "connecting") return "connecting"
229
+ if (socketState === "idle") return "idle"
230
+ const lastSettled = settledPulseState(pulse)
231
+ if (lastSettled === "idle") return "checking"
232
+ if (lastSettled === "error") return "degraded"
233
+ return pulseIsFresh(pulse, now) ? "open" : "degraded"
234
+ }
235
+ const emptyPulseSnapshot = (): PulseSnapshot => ({ state: "idle", lastSettledState: "idle", at: 0, ms: 0, error: "" })
210
236
 
211
237
  export const sanitizeProfile = (profile?: PeerProfile): PeerProfile => ({
212
238
  geo: {
@@ -389,6 +415,7 @@ export class SendSession {
389
415
 
390
416
  private autoAcceptIncoming: boolean
391
417
  private autoSaveIncoming: boolean
418
+ private readonly overwriteIncoming: boolean
392
419
  private readonly reconnectSocket: boolean
393
420
  private iceServers: RTCIceServer[]
394
421
  private extraTurnServers: RTCIceServer[]
@@ -405,6 +432,7 @@ export class SendSession {
405
432
  private socketToken = 0
406
433
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null
407
434
  private peerStatsTimer: ReturnType<typeof setInterval> | null = null
435
+ private pulseTimer: ReturnType<typeof setInterval> | null = null
408
436
  private readonly pendingRtcCloses = new Set<Promise<void>>()
409
437
  private lifecycleAbortController: AbortController | null = null
410
438
  private stopped = false
@@ -418,6 +446,7 @@ export class SendSession {
418
446
  this.peerSelectionMemory = config.peerSelectionMemory ?? new Map()
419
447
  this.autoAcceptIncoming = !!config.autoAcceptIncoming
420
448
  this.autoSaveIncoming = !!config.autoSaveIncoming
449
+ this.overwriteIncoming = !!config.overwriteIncoming
421
450
  this.reconnectSocket = config.reconnectSocket ?? true
422
451
  this.extraTurnServers = turnServers(config.turnUrls ?? [], config.turnUsername, config.turnCredential)
423
452
  this.iceServers = [...BASE_ICE_SERVERS, ...this.extraTurnServers]
@@ -460,6 +489,7 @@ export class SendSession {
460
489
  this.lifecycleAbortController?.abort()
461
490
  this.lifecycleAbortController = typeof AbortController === "function" ? new AbortController() : null
462
491
  this.startPeerStatsPolling()
492
+ this.startPulsePolling()
463
493
  void this.loadLocalProfile()
464
494
  void this.probePulse()
465
495
  this.connectSocket()
@@ -469,6 +499,7 @@ export class SendSession {
469
499
  async close() {
470
500
  this.stopped = true
471
501
  this.stopPeerStatsPolling()
502
+ this.stopPulsePolling()
472
503
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
473
504
  this.reconnectTimer = null
474
505
  this.lifecycleAbortController?.abort()
@@ -763,7 +794,7 @@ export class SendSession {
763
794
  if (transfer.savedPath) return transfer.savedPath
764
795
  if (!transfer.data && transfer.buffers?.length) transfer.data = Buffer.concat(transfer.buffers)
765
796
  if (!transfer.data) return null
766
- transfer.savedPath = await saveIncomingFile(this.saveDir, transfer.name, transfer.data)
797
+ transfer.savedPath = await saveIncomingFile(this.saveDir, transfer.name, transfer.data, this.overwriteIncoming)
767
798
  transfer.savedAt ||= Date.now()
768
799
  this.emitTransferSaved(transfer)
769
800
  this.notify()
@@ -788,8 +819,8 @@ export class SendSession {
788
819
  }
789
820
 
790
821
  private async createIncomingDiskState(fileName: string): Promise<IncomingDiskState> {
791
- const finalPath = await uniqueOutputPath(this.saveDir, fileName || "download", this.reservedSavePaths)
792
- this.reservedSavePaths.add(finalPath)
822
+ const finalPath = await incomingOutputPath(this.saveDir, fileName || "download", this.overwriteIncoming, this.reservedSavePaths)
823
+ if (!this.overwriteIncoming) this.reservedSavePaths.add(finalPath)
793
824
  for (let attempt = 0; ; attempt += 1) {
794
825
  const tempPath = `${finalPath}.part.${uid(6)}${attempt ? `.${attempt}` : ""}`
795
826
  try {
@@ -805,7 +836,7 @@ export class SendSession {
805
836
  }
806
837
  } catch (error) {
807
838
  if ((error as NodeJS.ErrnoException | undefined)?.code === "EEXIST") continue
808
- this.reservedSavePaths.delete(finalPath)
839
+ if (!this.overwriteIncoming) this.reservedSavePaths.delete(finalPath)
809
840
  throw error
810
841
  }
811
842
  }
@@ -875,12 +906,13 @@ export class SendSession {
875
906
  disk.closed = true
876
907
  await disk.handle.close()
877
908
  }
878
- if (await pathExists(finalPath)) {
909
+ if (!this.overwriteIncoming && await pathExists(finalPath)) {
879
910
  this.reservedSavePaths.delete(finalPath)
880
911
  finalPath = await uniqueOutputPath(this.saveDir, transfer.name || "download", this.reservedSavePaths)
881
912
  this.reservedSavePaths.add(finalPath)
882
913
  }
883
- await rename(disk.tempPath, finalPath)
914
+ if (this.overwriteIncoming) await replaceOutputPath(disk.tempPath, finalPath)
915
+ else await rename(disk.tempPath, finalPath)
884
916
  transfer.savedPath = finalPath
885
917
  transfer.savedAt ||= Date.now()
886
918
  return finalPath
@@ -952,6 +984,17 @@ export class SendSession {
952
984
  this.peerStatsTimer = null
953
985
  }
954
986
 
987
+ private startPulsePolling() {
988
+ if (this.pulseTimer) return
989
+ this.pulseTimer = setInterval(() => void this.probePulse(), PULSE_PROBE_INTERVAL_MS)
990
+ }
991
+
992
+ private stopPulsePolling() {
993
+ if (!this.pulseTimer) return
994
+ clearInterval(this.pulseTimer)
995
+ this.pulseTimer = null
996
+ }
997
+
955
998
  private async refreshPeerStats() {
956
999
  if (this.stopped) return
957
1000
  let dirty = false
@@ -1156,10 +1199,10 @@ export class SendSession {
1156
1199
  const response = await fetch(SIGNAL_PULSE_URL, { cache: "no-store", signal: timeoutSignal(3500, this.lifecycleAbortController?.signal) })
1157
1200
  if (!response.ok) throw new Error(`pulse ${response.status}`)
1158
1201
  if (this.stopped) return
1159
- this.pulse = { state: "open", at: Date.now(), ms: performance.now() - startedAt, error: "" }
1202
+ this.pulse = { state: "open", lastSettledState: "open", at: Date.now(), ms: performance.now() - startedAt, error: "" }
1160
1203
  } catch (error) {
1161
1204
  if (this.stopped) return
1162
- this.pulse = { state: "error", at: Date.now(), ms: 0, error: `${error}` }
1205
+ this.pulse = { state: "error", lastSettledState: "error", at: Date.now(), ms: 0, error: `${error}` }
1163
1206
  this.pushLog("pulse:error", { error: `${error}` }, "error")
1164
1207
  }
1165
1208
  if (this.stopped) return
package/src/index.ts CHANGED
@@ -67,6 +67,7 @@ const TURN_OPTIONS = [
67
67
  ["--turn-username <value>", "custom TURN username"],
68
68
  ["--turn-credential <value>", "custom TURN credential"],
69
69
  ] as const
70
+ const OVERWRITE_OPTION = ["--overwrite", "overwrite same-name saved files instead of creating copies"] as const
70
71
  const SAVE_DIR_OPTION = ["--save-dir <dir>", "save directory"] as const
71
72
  const TUI_TOGGLE_OPTIONS = [
72
73
  ["--clean <0|1>", "show only active peers when 1; show terminal peers too when 0"],
@@ -103,6 +104,7 @@ export const sessionConfigFrom = (options: Record<string, unknown>, defaults: {
103
104
  saveDir: resolve(`${options.saveDir ?? process.env.SEND_SAVE_DIR ?? "."}`),
104
105
  autoAcceptIncoming: accept ?? defaults.autoAcceptIncoming ?? false,
105
106
  autoSaveIncoming: save ?? defaults.autoSaveIncoming ?? false,
107
+ overwriteIncoming: !!options.overwrite,
106
108
  turnUrls: splitList(options.turnUrl ?? process.env.SEND_TURN_URL),
107
109
  turnUsername: `${options.turnUsername ?? process.env.SEND_TURN_USERNAME ?? ""}`.trim() || undefined,
108
110
  turnCredential: `${options.turnCredential ?? process.env.SEND_TURN_CREDENTIAL ?? ""}`.trim() || undefined,
@@ -302,6 +304,7 @@ export const createCli = (handlers: CliHandlers = defaultCliHandlers) => {
302
304
  addOptions(cli.command("accept", "receive and save files"), [
303
305
  ...ROOM_SELF_OPTIONS,
304
306
  SAVE_DIR_OPTION,
307
+ OVERWRITE_OPTION,
305
308
  ["--once", "exit after the first saved incoming transfer"],
306
309
  ["--json", "emit ndjson events"],
307
310
  ...TURN_OPTIONS,
@@ -312,6 +315,7 @@ export const createCli = (handlers: CliHandlers = defaultCliHandlers) => {
312
315
  ...TUI_TOGGLE_OPTIONS,
313
316
  ["--events", "show the event log pane"],
314
317
  SAVE_DIR_OPTION,
318
+ OVERWRITE_OPTION,
315
319
  ...TURN_OPTIONS,
316
320
  ]).action(handlers.tui)
317
321
 
package/src/tui/app.ts CHANGED
@@ -2,7 +2,7 @@ import { resolve } from "node:path"
2
2
  import { BACKEND_RAW_WRITE_MARKER, rgb, ui, type BackendRawWrite, type BadgeVariant, type TextStyle, type UiEvent, type VNode } from "@rezi-ui/core"
3
3
  import { createNodeApp } from "@rezi-ui/node"
4
4
  import { inspectLocalFile } from "../core/files"
5
- import { isSessionAbortedError, SendSession, type PeerSnapshot, type SessionConfig, type SessionSnapshot, type TransferSnapshot } from "../core/session"
5
+ import { isSessionAbortedError, SendSession, signalMetricState, type PeerSnapshot, type SessionConfig, type SessionSnapshot, type TransferSnapshot } from "../core/session"
6
6
  import { cleanLocalId, cleanName, cleanRoom, displayPeerName, fallbackName, formatBytes, type LogEntry, peerDefaultsToken, type PeerProfile, uid } from "../core/protocol"
7
7
  import { FILE_SEARCH_VISIBLE_ROWS, type FileSearchEvent, type FileSearchMatch, type FileSearchRequest } from "./file-search-protocol"
8
8
  import { deriveFileSearchScope, formatFileSearchDisplayPath, normalizeSearchQuery, offsetFileSearchMatchIndices } from "./file-search"
@@ -72,6 +72,7 @@ export interface TuiState {
72
72
  autoOfferOutgoing: boolean
73
73
  autoAcceptIncoming: boolean
74
74
  autoSaveIncoming: boolean
75
+ overwriteIncoming: boolean
75
76
  hideTerminalPeers: boolean
76
77
  eventsExpanded: boolean
77
78
  offeringDrafts: boolean
@@ -133,8 +134,13 @@ const DEFAULT_SAVE_DIR = resolve(process.cwd())
133
134
  const ABOUT_ELEFUNC_URL = "https://rtme.sh/send"
134
135
  const ABOUT_TITLE = "About Send"
135
136
  const ABOUT_INTRO = "Peer-to-Peer Transfers – Web & CLI"
136
- const ABOUT_SUMMARY = "Join the same room, see who is there, and offer files directly to selected peers."
137
- const ABOUT_RUNTIME = "Send uses lightweight signaling to discover peers and negotiate WebRTC. Files move over WebRTC data channels, using direct paths when possible and TURN relay when needed."
137
+ const ABOUT_BULLETS = [
138
+ "• Join a room, see who is there, and filter or select exactly which peers to target before offering files.",
139
+ "• File data does not travel through the signaling service; Send uses lightweight signaling to discover peers and negotiate WebRTC, then transfers directly peer-to-peer when possible, with TURN relay when needed.",
140
+ "• Incoming transfers can be auto-accepted and auto-saved, and same-name files can either stay as numbered copies or overwrite the original when that mode is enabled.",
141
+ "• The CLI streams incoming saves straight to disk in the current save directory, with overwrite available through the CLI flag.",
142
+ "• Other features include copyable web and CLI invites, rendered-peer filtering and selection, TURN sharing, and live connection insight like signaling state, RTT, data state, and path labels.",
143
+ ] as const
138
144
  const COPY_SERVICE_URL = "https://copy.rt.ht/"
139
145
  const TRANSFER_DIRECTION_ARROW = {
140
146
  out: { glyph: "↗", style: { fg: rgb(170, 217, 76), bold: true } },
@@ -169,7 +175,7 @@ type BunSpawn = (cmd: string[], options: {
169
175
  stderr?: "pipe" | "inherit" | "ignore"
170
176
  }) => { unref?: () => void }
171
177
  type BunLike = { spawn?: BunSpawn }
172
- type ShareUrlState = Pick<TuiState, "snapshot" | "hideTerminalPeers" | "autoAcceptIncoming" | "autoOfferOutgoing" | "autoSaveIncoming">
178
+ type ShareUrlState = Pick<TuiState, "snapshot" | "hideTerminalPeers" | "autoAcceptIncoming" | "autoOfferOutgoing" | "autoSaveIncoming" | "overwriteIncoming">
173
179
  type ShareCliState = ShareUrlState & Pick<TuiState, "sessionSeed" | "eventsExpanded">
174
180
  const shellQuote = (value: string) => safeShellArgPattern.test(value) ? value : `'${value.replaceAll("'", `'\"'\"'`)}'`
175
181
  const appendCliFlag = (args: string[], flag: string, value?: string | null) => {
@@ -185,10 +191,12 @@ const SHARE_TOGGLE_FLAGS = [
185
191
  ] as const satisfies readonly (readonly [string, keyof ShareUrlState, string])[]
186
192
  const appendToggleCliFlags = (args: string[], state: ShareUrlState) => {
187
193
  for (const [, stateKey, flag] of SHARE_TOGGLE_FLAGS) if (!state[stateKey]) appendCliFlag(args, flag, "0")
194
+ if (state.overwriteIncoming) args.push("--overwrite")
188
195
  }
189
196
  const buildHashParams = (state: ShareUrlState, omitDefaults = false) => {
190
197
  const params = new URLSearchParams({ room: cleanRoom(state.snapshot.room) })
191
198
  for (const [key, stateKey] of SHARE_TOGGLE_FLAGS) if (!omitDefaults || !state[stateKey]) params.set(key, hashBool(state[stateKey]))
199
+ if (!omitDefaults || state.overwriteIncoming) params.set("overwrite", hashBool(state.overwriteIncoming))
192
200
  return params
193
201
  }
194
202
  const shareTurnCliArgs = (sessionSeed: SessionSeed) => {
@@ -393,7 +401,7 @@ const statusVariant = (status: TransferSnapshot["status"]): BadgeVariant => ({
393
401
  cancelling: "warning",
394
402
  "awaiting-done": "info",
395
403
  }[status] || "default") as BadgeVariant
396
- const statusToneVariant = (value: string): BadgeVariant => ({
404
+ export const statusToneVariant = (value: string): BadgeVariant => ({
397
405
  open: "success",
398
406
  connected: "success",
399
407
  complete: "success",
@@ -411,6 +419,7 @@ const statusToneVariant = (value: string): BadgeVariant => ({
411
419
  pending: "warning",
412
420
  cancelling: "warning",
413
421
  checking: "warning",
422
+ degraded: "warning",
414
423
  disconnected: "warning",
415
424
  left: "warning",
416
425
  rejected: "warning",
@@ -730,6 +739,7 @@ export const createInitialTuiState = (initialConfig: SessionConfig, showEvents =
730
739
  const sessionSeed = normalizeSessionSeed(initialConfig)
731
740
  const autoAcceptIncoming = initialConfig.autoAcceptIncoming ?? true
732
741
  const autoSaveIncoming = initialConfig.autoSaveIncoming ?? true
742
+ const overwriteIncoming = !!initialConfig.overwriteIncoming
733
743
  const peerSelectionByRoom = new Map<string, Map<string, boolean>>()
734
744
  const session = makeSession(sessionSeed, autoAcceptIncoming, autoSaveIncoming, roomPeerSelectionMemory(peerSelectionByRoom, sessionSeed.room))
735
745
  const focusState = deriveBootFocusState(sessionSeed.name)
@@ -753,6 +763,7 @@ export const createInitialTuiState = (initialConfig: SessionConfig, showEvents =
753
763
  autoOfferOutgoing: launchOptions.offer ?? true,
754
764
  autoAcceptIncoming,
755
765
  autoSaveIncoming,
766
+ overwriteIncoming,
756
767
  hideTerminalPeers: launchOptions.clean ?? true,
757
768
  eventsExpanded: showEvents,
758
769
  offeringDrafts: false,
@@ -854,8 +865,7 @@ const renderAboutModal = (_state: TuiState, actions: TuiActions) => {
854
865
  title: ABOUT_TITLE,
855
866
  content: ui.column({ gap: 1 }, [
856
867
  ui.text(ABOUT_INTRO, { id: "about-intro", variant: "heading", wrap: true }),
857
- ui.text(ABOUT_SUMMARY, { id: "about-summary", wrap: true }),
858
- ui.text(ABOUT_RUNTIME, { id: "about-runtime", wrap: true }),
868
+ ...ABOUT_BULLETS.map((line, index) => ui.text(line, { id: `about-bullet-${index + 1}`, wrap: true })),
859
869
  ]),
860
870
  actions: [
861
871
  ui.link({
@@ -957,8 +967,7 @@ const renderSelfCard = (state: TuiState, actions: TuiActions) => denseSection({
957
967
  ui.text(`-${state.snapshot.localId}`),
958
968
  ]),
959
969
  ui.row({ gap: 0, wrap: true }, [
960
- renderSelfMetric("Signaling", state.snapshot.socketState),
961
- renderSelfMetric("Pulse", state.snapshot.pulse.state),
970
+ renderSelfMetric("Signaling", signalMetricState(state.snapshot.socketState, state.snapshot.pulse)),
962
971
  renderSelfMetric("TURN", state.snapshot.turnState),
963
972
  ]),
964
973
  ui.column({ gap: 0 }, [
@@ -1000,10 +1009,10 @@ const renderPeerRow = (peer: PeerSnapshot, turnShareEnabled: boolean, actions: T
1000
1009
  ]),
1001
1010
  ui.row({ gap: 0 }, [
1002
1011
  renderPeerMetric("RTT", formatPeerRtt(peer.rttMs)),
1003
- renderPeerMetric("Data", peer.dataState, true),
1012
+ renderPeerMetric("TURN", peer.turnState, true),
1004
1013
  ]),
1005
1014
  ui.row({ gap: 0 }, [
1006
- renderPeerMetric("TURN", peer.turnState, true),
1015
+ renderPeerMetric("Data", peer.dataState, true),
1007
1016
  renderPeerMetric("Path", peer.pathLabel || "—"),
1008
1017
  ]),
1009
1018
  ui.column({ gap: 0 }, [
@@ -1141,7 +1150,7 @@ const transferActionButtons = (transfer: TransferSnapshot, actions: TuiActions):
1141
1150
  return []
1142
1151
  }
1143
1152
 
1144
- const renderTransferFact = (label: string, value: string) => ui.box({ minWidth: 12 }, [
1153
+ const renderTransferFact = (label: string, value: string) => ui.box({ minWidth: 12, border: "single", borderStyle: METRIC_BORDER_STYLE }, [
1145
1154
  ui.column({ gap: 0 }, [
1146
1155
  ui.text(label, { variant: "caption" }),
1147
1156
  ui.text(value),
@@ -1676,6 +1685,7 @@ export const startTui = async (initialConfig: SessionConfig, launchOptions: TuiL
1676
1685
  filePreview: resetFilePreview(),
1677
1686
  drafts: [],
1678
1687
  offeringDrafts: false,
1688
+ overwriteIncoming: !!nextSeed.overwriteIncoming,
1679
1689
  ...(options.reseedBootFocus
1680
1690
  ? deriveBootFocusState(nextSeed.name, current.focusRequestEpoch + 1)
1681
1691
  : {