@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 +1 -1
- package/src/core/files.ts +18 -3
- package/src/core/session.ts +53 -10
- package/src/index.ts +4 -0
- package/src/tui/app.ts +22 -12
package/package.json
CHANGED
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
|
|
105
|
-
|
|
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
|
}
|
package/src/core/session.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
137
|
-
|
|
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(
|
|
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("
|
|
1012
|
+
renderPeerMetric("TURN", peer.turnState, true),
|
|
1004
1013
|
]),
|
|
1005
1014
|
ui.row({ gap: 0 }, [
|
|
1006
|
-
renderPeerMetric("
|
|
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
|
: {
|