feat(app): expose local sharing and verified transfers

Add a durable, acknowledged Local network sharing switch with fail-closed
hydration, serialized mutation, and redacted ephemeral-identity diagnostics.
Keep local Call-to-Play state available while gating every network action on the
effective sharing generation.

Render revisioned verification, invalid-source retry, and sticky source
exhaustion states. Preserve opaque attempt IDs through progress delivery so
out-of-order webview events cannot attach stale bytes to a successor transfer,
and keep terminal exhaustion visible after the last source departs.

Own listeners, native invokes, persistence, dialogs, and companion-window
creation through webview close. Late creation is settled and cleaned before the
parent realm is destroyed.

Test Plan:
- `just frontend-test` -- passed (91/91)
- `just build` -- passed with TypeScript, Vite, and release Tauri compilation
- `just test` -- passed on the completed stack (708 workspace tests)
- `just clippy` -- passed on the completed stack
- `git diff --cached --check` -- passed
This commit is contained in:
ddidderr committed 2026-08-10 13:59:40 +02:00
1 parent 60fd7ba0c2
commit 71dbf27d8b
42 files changed
+7457 -2101

No files matched your search

@@ -19,6 +19,7 @@ import {
rowFromPayload,
rowsFromHistory,
} from './lib/mainLogs';
import { AsyncOwner, windowAsyncScope } from './lib/asyncOwnership';
import './MainLogsWindow.css';
@@ -43,6 +44,8 @@ export const MainLogsWindow = () => {
const pausedRef = useRef(false);
const lastHistorySequenceRef = useRef(0);
const historyLineCountsRef = useRef<Map<string, number>>(new Map());
const viewOwnerRef = useRef<AsyncOwner | null>(null);
const copyStatusTimerRef = useRef<number | undefined>(undefined);
const appendVisibleRows = useCallback((rows: MainLogRow[]) => {
setLogs(current => capLogRows([...current, ...rows]));
@@ -54,10 +57,18 @@ export const MainLogsWindow = () => {
}, []);
useEffect(() => {
let cancelled = false;
let unlisten: (() => void) | undefined;
const owner = new AsyncOwner(error => {
console.error('Failed to unregister main log listener:', error);
});
viewOwnerRef.current = owner;
owner.own(() => {
if (copyStatusTimerRef.current !== undefined) {
window.clearTimeout(copyStatusTimerRef.current);
copyStatusTimerRef.current = undefined;
}
});
const handleIncomingRow = (row: MainLogRow) => {
const handleIncomingRow = owner.guard((row: MainLogRow) => {
if (!historyLoadedRef.current) {
initialBufferRef.current = capLogRows([...initialBufferRef.current, row]);
return;
@@ -76,43 +87,55 @@ export const MainLogsWindow = () => {
return;
}
appendVisibleRows([row]);
};
});
const setup = async () => {
try {
unlisten = await listen<MainLogLinePayload>('main-log-line', event => {
handleIncomingRow(rowFromPayload(event.payload));
});
const history = await invoke<MainLogHistoryPayload>('get_main_logs');
if (cancelled) return;
lastHistorySequenceRef.current = history.lastSequence;
const historyRows = rowsFromHistory(history.contents);
const historyLineCounts = lineCountsFromRows(historyRows);
const liveRows = dedupeBufferedRows(
historyLineCounts,
initialBufferRef.current,
lastHistorySequenceRef.current,
const registered = await owner.register(() =>
listen<MainLogLinePayload>('main-log-line', event => {
handleIncomingRow(rowFromPayload(event.payload));
})
);
initialBufferRef.current = [];
historyLineCountsRef.current = historyLineCounts;
historyLoadedRef.current = true;
if (pausedRef.current) {
setLogs(capLogRows(historyRows));
bufferPausedRows(liveRows);
} else {
setLogs(capLogRows([...historyRows, ...liveRows]));
}
setLoadError(null);
if (!registered) return;
} catch (err) {
if (!cancelled) {
if (owner.isActive()) {
historyLoadedRef.current = true;
setLoadError(err instanceof Error ? err.message : String(err));
setLoading(false);
}
} finally {
if (!cancelled) {
return;
}
try {
await owner.applyIfActive(
() => invoke<MainLogHistoryPayload>('get_main_logs'),
history => {
lastHistorySequenceRef.current = history.lastSequence;
const historyRows = rowsFromHistory(history.contents);
const historyLineCounts = lineCountsFromRows(historyRows);
const liveRows = dedupeBufferedRows(
historyLineCounts,
initialBufferRef.current,
lastHistorySequenceRef.current,
);
initialBufferRef.current = [];
historyLineCountsRef.current = historyLineCounts;
historyLoadedRef.current = true;
if (pausedRef.current) {
setLogs(capLogRows(historyRows));
bufferPausedRows(liveRows);
} else {
setLogs(capLogRows([...historyRows, ...liveRows]));
}
setLoadError(null);
setLoading(false);
},
);
} catch (err) {
if (owner.isActive()) {
historyLoadedRef.current = true;
setLoadError(err instanceof Error ? err.message : String(err));
setLoading(false);
}
}
@@ -121,12 +144,12 @@ export const MainLogsWindow = () => {
void setup();
return () => {
cancelled = true;
windowAsyncScope.adopt(owner.dispose());
if (viewOwnerRef.current === owner) viewOwnerRef.current = null;
historyLoadedRef.current = false;
initialBufferRef.current = [];
lastHistorySequenceRef.current = 0;
historyLineCountsRef.current = new Map();
unlisten?.();
};
}, [appendVisibleRows, bufferPausedRows]);
@@ -156,9 +179,10 @@ export const MainLogsWindow = () => {
const viewport = viewportRef.current;
if (!viewport) return;
requestAnimationFrame(() => {
const frame = requestAnimationFrame(() => {
viewport.scrollTop = viewport.scrollHeight;
});
return () => cancelAnimationFrame(frame);
}, [autoScroll, filteredRows.length, lastVisibleRow?.id]);
const flushPausedRows = useCallback(() => {
@@ -189,13 +213,24 @@ export const MainLogsWindow = () => {
}, []);
const copyFilteredLogs = useCallback(async () => {
const owner = viewOwnerRef.current;
if (!owner) return;
try {
await navigator.clipboard.writeText(filteredRows.map(row => row.line).join('\n'));
setCopyStatus('Copied');
if (!await owner.applyIfActive(
() => navigator.clipboard.writeText(filteredRows.map(row => row.line).join('\n')),
() => setCopyStatus('Copied'),
)) return;
} catch {
if (!owner.isActive()) return;
setCopyStatus('Copy failed');
}
window.setTimeout(() => setCopyStatus(null), 1600);
if (copyStatusTimerRef.current !== undefined) {
window.clearTimeout(copyStatusTimerRef.current);
}
copyStatusTimerRef.current = window.setTimeout(owner.guard(() => {
copyStatusTimerRef.current = undefined;
setCopyStatus(null);
}), 1600);
}, [filteredRows]);
return (
@@ -219,7 +254,7 @@ export const MainLogsWindow = () => {
</button>
<button
className="settings-button"
onClick={() => void copyFilteredLogs()}
onClick={() => windowAsyncScope.adopt(copyFilteredLogs())}
disabled={filteredRows.length === 0}
>
Copy